From c91abe48bae12ce597ce737c19b8e71e4ab8603e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 20 Feb 2020 18:51:41 -0300 Subject: [PATCH 001/823] Assorted improvements following up #6658 --- src/_pytest/_code/code.py | 21 ++++++++++++++------- src/_pytest/_io/__init__.py | 2 +- testing/conftest.py | 4 +++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index fd38b950cdb..9d65289df14 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1048,28 +1048,35 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None: character, as doing so might break line continuations. """ - indent_size = 4 - - def is_fail(line): - return line.startswith("{} ".format(FormattedExcinfo.fail_marker)) - if not self.lines: return # separate indents and source lines that are not failures: we want to # highlight the code but not the indentation, which may contain markers # such as "> assert 0" + fail_marker = "{} ".format(FormattedExcinfo.fail_marker) + indent_size = len(fail_marker) indents = [] source_lines = [] + failure_lines = [] + seeing_failures = False for line in self.lines: - if not is_fail(line): + is_source_line = not line.startswith(fail_marker) + if is_source_line: + assert not seeing_failures, ( + "Unexpected failure lines between source lines:\n" + + "\n".join(self.lines) + ) indents.append(line[:indent_size]) source_lines.append(line[indent_size:]) + else: + seeing_failures = True + failure_lines.append(line) tw._write_source(source_lines, indents) # failure lines are always completely red and bold - for line in (x for x in self.lines if is_fail(x)): + for line in failure_lines: tw.line(line, bold=True, red=True) def toterminal(self, tw: TerminalWriter) -> None: diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index f56579806cc..28ddc7b78ed 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -26,7 +26,7 @@ def _write_source(self, lines: List[str], indents: Sequence[str] = ()) -> None: self.line(indent + new_line) def _highlight(self, source): - """Highlight the given source code according to the "code_highlight" option""" + """Highlight the given source code if we have markup support""" if not self.hasmarkup: return source try: diff --git a/testing/conftest.py b/testing/conftest.py index 90cdcb869fd..58386b162bb 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -192,6 +192,8 @@ def requires_ordered_markup(cls, result: RunResult): output = result.stdout.str() assert "test session starts" in output assert "\x1b[1m" in output - pytest.skip("doing limited testing because lacking ordered markup") + pytest.skip( + "doing limited testing because lacking ordered markup on py35" + ) return ColorMapping From 077001fe5c0522381c236d6d28f0f232ad9b4286 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 22 Feb 2020 23:31:08 +0100 Subject: [PATCH 002/823] tests: simplify test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives --- testing/test_config.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/testing/test_config.py b/testing/test_config.py index 9f6042e4b9b..18e4c388f88 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1450,27 +1450,16 @@ def test_func(): def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives( self, testdir ): - subdirectory = testdir.tmpdir.join("subdirectory") - subdirectory.mkdir() - testdir.makeconftest( - """ - pass - """ - ) - testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py")) - - testdir.makeconftest( - """ - import warnings - warnings.filterwarnings('always', category=DeprecationWarning) - pytest_plugins=['capture'] - """ - ) testdir.makepyfile( - """ - def test_func(): - pass - """ + "def test_func(): pass", + **{ + "subdirectory/conftest": "pass", + "conftest": """ + import warnings + warnings.filterwarnings('always', category=DeprecationWarning) + pytest_plugins=['capture'] + """, + } ) res = testdir.runpytest_subprocess() assert res.ret == 0 From eac933acdede9aa623791f710199d9db2513a692 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 25 Feb 2020 17:24:07 +0100 Subject: [PATCH 003/823] assertion: rewrite: only catch EnvironmentError This was changed unintentionally in 45c4a8fb3 (pytest 5.3.0), but only EnvironmentErrors might have `errno`. Since that is not really guaranteed and it is good to have more information this uses the string representation of the exc in the trace message. --- src/_pytest/assertion/rewrite.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ab5e63a1e0c..8ce3963d225 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -276,7 +276,7 @@ def _write_pyc(state, co, source_stat, pyc): with atomic_write(fspath(pyc), mode="wb", overwrite=True) as fp: _write_pyc_fp(fp, source_stat, co) except EnvironmentError as e: - state.trace("error writing pyc file at {}: errno={}".format(pyc, e.errno)) + state.trace("error writing pyc file at {}: {}".format(pyc, e)) # we ignore any failure to write the cache file # there are many reasons, permission-denied, pycache dir being a # file etc. @@ -299,8 +299,8 @@ def _write_pyc(state, co, source_stat, pyc): try: _write_pyc_fp(fp, source_stat, co) os.rename(proc_pyc, fspath(pyc)) - except BaseException as e: - state.trace("error writing pyc file at {}: errno={}".format(pyc, e.errno)) + except EnvironmentError as e: + state.trace("error writing pyc file at {}: {}".format(pyc, e)) # we ignore any failure to write the cache file # there are many reasons, permission-denied, pycache dir being a # file etc. From d2f9a73a299c0c15280e072c9b3183870e0b1e4b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 25 Feb 2020 02:15:26 +0100 Subject: [PATCH 004/823] typing: get_dirs_from_args --- src/_pytest/config/findpaths.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index fb84160c1ff..d6307108472 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -86,14 +86,14 @@ def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local: return common_ancestor -def get_dirs_from_args(args): - def is_option(x): - return str(x).startswith("-") +def get_dirs_from_args(args: List[str]) -> List[py.path.local]: + def is_option(x: str) -> bool: + return x.startswith("-") - def get_file_part_from_node_id(x): - return str(x).split("::")[0] + def get_file_part_from_node_id(x: str) -> str: + return x.split("::")[0] - def get_dir_from_path(path): + def get_dir_from_path(path: py.path.local) -> py.path.local: if path.isdir(): return path return py.path.local(path.dirname) From 6c236767e03b2d3005604ed668072be0687325f3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 25 Feb 2020 03:32:46 +0100 Subject: [PATCH 005/823] Adjust/fix test_config: use strs with determine_setup --- testing/test_config.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/testing/test_config.py b/testing/test_config.py index be728c0fc69..42c1a85b80d 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -3,6 +3,8 @@ import sys import textwrap +import py.path + import _pytest._code import pytest from _pytest.compat import importlib_metadata @@ -900,55 +902,55 @@ def test_simple_noini(self, tmpdir): assert get_common_ancestor([no_path.join("a")]) == tmpdir @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) - def test_with_ini(self, tmpdir, name) -> None: + def test_with_ini(self, tmpdir: py.path.local, name: str) -> None: inifile = tmpdir.join(name) inifile.write("[pytest]\n" if name != "setup.cfg" else "[tool:pytest]\n") a = tmpdir.mkdir("a") b = a.mkdir("b") - for args in ([tmpdir], [a], [b]): + for args in ([str(tmpdir)], [str(a)], [str(b)]): rootdir, parsed_inifile, _ = determine_setup(None, args) assert rootdir == tmpdir assert parsed_inifile == inifile - rootdir, parsed_inifile, _ = determine_setup(None, [b, a]) + rootdir, parsed_inifile, _ = determine_setup(None, [str(b), str(a)]) assert rootdir == tmpdir assert parsed_inifile == inifile @pytest.mark.parametrize("name", "setup.cfg tox.ini".split()) - def test_pytestini_overrides_empty_other(self, tmpdir, name) -> None: + def test_pytestini_overrides_empty_other(self, tmpdir: py.path.local, name) -> None: inifile = tmpdir.ensure("pytest.ini") a = tmpdir.mkdir("a") a.ensure(name) - rootdir, parsed_inifile, _ = determine_setup(None, [a]) + rootdir, parsed_inifile, _ = determine_setup(None, [str(a)]) assert rootdir == tmpdir assert parsed_inifile == inifile - def test_setuppy_fallback(self, tmpdir) -> None: + def test_setuppy_fallback(self, tmpdir: py.path.local) -> None: a = tmpdir.mkdir("a") a.ensure("setup.cfg") tmpdir.ensure("setup.py") - rootdir, inifile, inicfg = determine_setup(None, [a]) + rootdir, inifile, inicfg = determine_setup(None, [str(a)]) assert rootdir == tmpdir assert inifile is None assert inicfg == {} - def test_nothing(self, tmpdir, monkeypatch) -> None: + def test_nothing(self, tmpdir: py.path.local, monkeypatch) -> None: monkeypatch.chdir(str(tmpdir)) - rootdir, inifile, inicfg = determine_setup(None, [tmpdir]) + rootdir, inifile, inicfg = determine_setup(None, [str(tmpdir)]) assert rootdir == tmpdir assert inifile is None assert inicfg == {} - def test_with_specific_inifile(self, tmpdir) -> None: + def test_with_specific_inifile(self, tmpdir: py.path.local) -> None: inifile = tmpdir.ensure("pytest.ini") - rootdir, _, _ = determine_setup(inifile, [tmpdir]) + rootdir, _, _ = determine_setup(str(inifile), [str(tmpdir)]) assert rootdir == tmpdir def test_with_arg_outside_cwd_without_inifile(self, tmpdir, monkeypatch) -> None: monkeypatch.chdir(str(tmpdir)) a = tmpdir.mkdir("a") b = tmpdir.mkdir("b") - rootdir, inifile, _ = determine_setup(None, [a, b]) + rootdir, inifile, _ = determine_setup(None, [str(a), str(b)]) assert rootdir == tmpdir assert inifile is None @@ -956,7 +958,7 @@ def test_with_arg_outside_cwd_with_inifile(self, tmpdir) -> None: a = tmpdir.mkdir("a") b = tmpdir.mkdir("b") inifile = a.ensure("pytest.ini") - rootdir, parsed_inifile, _ = determine_setup(None, [a, b]) + rootdir, parsed_inifile, _ = determine_setup(None, [str(a), str(b)]) assert rootdir == a assert inifile == parsed_inifile From 6092d3c6e1c99be7a424db72ea144aa8caa0862e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 25 Feb 2020 03:47:06 +0100 Subject: [PATCH 006/823] typing: parseconfig --- src/_pytest/pytester.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index f383e51f652..1d6fbbd960d 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -29,6 +29,7 @@ from _pytest.capture import SysCapture from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin +from _pytest.config import Config from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest from _pytest.main import Session @@ -975,7 +976,7 @@ def _ensure_basetemp(self, args): args.append("--basetemp=%s" % self.tmpdir.dirpath("basetemp")) return args - def parseconfig(self, *args): + def parseconfig(self, *args: Union[str, py.path.local]) -> Config: """Return a new pytest Config instance from given commandline args. This invokes the pytest bootstrapping code in _pytest.config to create @@ -991,7 +992,7 @@ def parseconfig(self, *args): import _pytest.config - config = _pytest.config._prepareconfig(args, self.plugins) + config = _pytest.config._prepareconfig(args, self.plugins) # type: Config # we don't know what the test will do with this half-setup config # object and thus we make sure it gets unconfigured properly in any # case (otherwise capturing could still be active, for example) From 8128f4b3b849d3ea42591f107017b008c168adf1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 25 Feb 2020 03:47:35 +0100 Subject: [PATCH 007/823] Fix test_write_pyc: passed list to parseconfig --- testing/test_assertrewrite.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 91a3a35e2e2..99328c6f94c 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -23,6 +23,7 @@ from _pytest.assertion.rewrite import rewrite_asserts from _pytest.config import ExitCode from _pytest.pathlib import Path +from _pytest.pytester import Testdir def setup_module(mod): @@ -956,11 +957,11 @@ def test_meta_path(): ) assert testdir.runpytest().ret == 0 - def test_write_pyc(self, testdir, tmpdir, monkeypatch): + def test_write_pyc(self, testdir: Testdir, tmpdir, monkeypatch) -> None: from _pytest.assertion.rewrite import _write_pyc from _pytest.assertion import AssertionState - config = testdir.parseconfig([]) + config = testdir.parseconfig() state = AssertionState(config, "rewrite") source_path = str(tmpdir.ensure("source.py")) pycpath = tmpdir.join("pyc").strpath From d99bfc18b85e243233e86360fbe6ec07e8287201 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 29 Feb 2020 11:39:33 +0100 Subject: [PATCH 008/823] Update src/_pytest/config/findpaths.py Co-Authored-By: Ran Benita --- src/_pytest/config/findpaths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index d6307108472..56b25f10fe5 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -86,7 +86,7 @@ def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local: return common_ancestor -def get_dirs_from_args(args: List[str]) -> List[py.path.local]: +def get_dirs_from_args(args: Iterable[str]) -> List[py.path.local]: def is_option(x: str) -> bool: return x.startswith("-") From 3d390940d1ad895527486ea69fd06d68732b677b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 6 Feb 2020 00:00:02 +0100 Subject: [PATCH 009/823] refer the node-from-parent deprecation documentation in the warning fixup: fix test for warning --- src/_pytest/deprecated.py | 5 ++++- testing/deprecated_test.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index db43c6ca3ef..cd9ed97d89c 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -36,7 +36,10 @@ NODE_USE_FROM_PARENT = UnformattedWarning( PytestDeprecationWarning, - "direct construction of {name} has been deprecated, please use {name}.from_parent", + "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n" + "See " + "https://docs.pytest.org/en/latest/deprecations.html#node-construction-changed-to-node-from-parent" + " for more details.", ) JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning( diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index b5c66d9f5f1..98c535aed6b 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -86,7 +86,7 @@ class MockConfig: ms = MockConfig() with pytest.warns( DeprecationWarning, - match="direct construction of .* has been deprecated, please use .*.from_parent", + match="Direct construction of .* has been deprecated, please use .*.from_parent.*", ) as w: nodes.Node(name="test", config=ms, session=ms, nodeid="None") assert w[0].lineno == inspect.currentframe().f_lineno - 1 From 5c1e56d3505e42e258a4aed4a38836ac82def810 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 18 Feb 2020 13:45:29 +0100 Subject: [PATCH 010/823] docs: from_parent - add minimal before/after example fixup: fix from_parent version --- doc/en/deprecations.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 732f92985f1..6956d0aaacf 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -29,7 +29,7 @@ Below is a complete list of all pytest features which are considered deprecated. Option ``--no-print-logs`` is deprecated and meant to be removed in a future release. If you use ``--no-print-logs``, please try out ``--show-capture`` and provide feedback. -``--show-capture`` command-line option was added in ``pytest 3.5.0` and allows to specify how to +``--show-capture`` command-line option was added in ``pytest 3.5.0`` and allows to specify how to display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default). @@ -39,9 +39,13 @@ Node Construction changed to ``Node.from_parent`` .. deprecated:: 5.4 -The construction of nodes new should use the named constructor ``from_parent``. +The construction of nodes now should use the named constructor ``from_parent``. This limitation in api surface intends to enable better/simpler refactoring of the collection tree. +This means that instead of :code:`MyItem(name="foo", parent=collector, obj=42)` +one now has to invoke :code:`MyItem.from_parent(collector, name="foo")`. + + ``junit_family`` default value change to "xunit2" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 3637d9eb3fde10f12abfe2966bcdd30fc9a8808e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 22 Feb 2020 17:09:16 +0100 Subject: [PATCH 011/823] followup: add note on from_parent kwargs --- doc/en/deprecations.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 6956d0aaacf..b707caa1310 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -45,6 +45,8 @@ This limitation in api surface intends to enable better/simpler refactoring of t This means that instead of :code:`MyItem(name="foo", parent=collector, obj=42)` one now has to invoke :code:`MyItem.from_parent(collector, name="foo")`. +Note that ``from_parent`` should only be called with keyword arguments for the parameters. + ``junit_family`` default value change to "xunit2" From 620d457756b519ba6b6c52c10412b84ea7c46ceb Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 2 Mar 2020 17:08:37 +0100 Subject: [PATCH 012/823] doc: add __tracebackhide__ label --- doc/en/example/simple.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 1a5c5b444cb..c25f4d53b4b 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -218,8 +218,10 @@ Or run it including the ``slow`` marked test: ============================ 2 passed in 0.12s ============================= +.. _`__tracebackhide__`: + Writing well integrated assertion helpers --------------------------------------------------- +----------------------------------------- .. regendoc:wipe From b40a9f9add7c53f86bbb7b6ea1695d92b56cdb13 Mon Sep 17 00:00:00 2001 From: earonesty Date: Wed, 27 Nov 2019 10:53:05 -0500 Subject: [PATCH 013/823] Export FixtureLookupError to top level --- src/pytest/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 33bc3d0fbe5..2f63164b3e8 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -15,6 +15,7 @@ from _pytest.fixtures import fillfixtures as _fillfuncargs from _pytest.fixtures import fixture from _pytest.fixtures import yield_fixture +from _pytest.fixtures import FixtureLookupError from _pytest.freeze_support import freeze_includes from _pytest.main import Session from _pytest.mark import MARK_GEN as mark From 0f00495548234f9c3253a923af4bed1f244e475d Mon Sep 17 00:00:00 2001 From: earonesty Date: Wed, 27 Nov 2019 11:02:43 -0500 Subject: [PATCH 014/823] Create 6285.feature.rst --- changelog/6285.feature.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog/6285.feature.rst diff --git a/changelog/6285.feature.rst b/changelog/6285.feature.rst new file mode 100644 index 00000000000..74c69122402 --- /dev/null +++ b/changelog/6285.feature.rst @@ -0,0 +1,2 @@ +Use `pytest.FixtureLookupError` to catch exceptions when calling `request.getfixturevalue`. + From 74cdff86f86ad8ee01b221acebd21fa61382561f Mon Sep 17 00:00:00 2001 From: earonesty Date: Wed, 27 Nov 2019 11:07:51 -0500 Subject: [PATCH 015/823] Update conftest.py --- .../sub1/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py index 79af4bc4790..be5adbeb6e5 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py @@ -3,5 +3,5 @@ @pytest.fixture def arg1(request): - with pytest.raises(Exception): + with pytest.raises(pytest.FixtureLookupError): request.getfixturevalue("arg2") From 7667ff51e7f3c9b2eb0b97e0e28390736163efdc Mon Sep 17 00:00:00 2001 From: earonesty Date: Wed, 27 Nov 2019 11:10:18 -0500 Subject: [PATCH 016/823] Update fixtures.py --- testing/python/fixtures.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index bfbe359515c..36e55a0e1de 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3,7 +3,6 @@ import pytest from _pytest import fixtures -from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureRequest from _pytest.pathlib import Path from _pytest.pytester import get_public_names @@ -654,7 +653,7 @@ def test_func(something): pass ) req = item._request - with pytest.raises(FixtureLookupError): + with pytest.raises(pytest.FixtureLookupError): req.getfixturevalue("notexists") val = req.getfixturevalue("something") assert val == 1 From 615474329d9d16269b2db5f95cc46db6129e68a4 Mon Sep 17 00:00:00 2001 From: earonesty Date: Wed, 27 Nov 2019 11:12:48 -0500 Subject: [PATCH 017/823] Update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index af0dc62c4d8..99f7f8b2d85 100644 --- a/AUTHORS +++ b/AUTHORS @@ -94,6 +94,7 @@ Elizaveta Shashkova Endre Galaczi Eric Hunsberger Eric Siegerman +Erik Aronesty Erik M. Bray Evan Kepner Fabien Zarifian From 9b8ed8d9ad2cf4022ff5c0a3a6c08ea96c2856a2 Mon Sep 17 00:00:00 2001 From: earonesty Date: Wed, 27 Nov 2019 11:24:40 -0500 Subject: [PATCH 018/823] Update pytest.py --- src/pytest/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 2f63164b3e8..7c8d834ca1c 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -63,6 +63,7 @@ "fail", "File", "fixture", + "FixtureLookupError", "freeze_includes", "Function", "hookimpl", From b2d54fe6b1d911349a9a2f8610e8882e48b65957 Mon Sep 17 00:00:00 2001 From: earonesty Date: Wed, 27 Nov 2019 11:28:35 -0500 Subject: [PATCH 019/823] Fix tox alpha order --- src/pytest/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 7c8d834ca1c..ec0298d00a5 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -14,8 +14,8 @@ from _pytest.debugging import pytestPDB as __pytestPDB from _pytest.fixtures import fillfixtures as _fillfuncargs from _pytest.fixtures import fixture -from _pytest.fixtures import yield_fixture from _pytest.fixtures import FixtureLookupError +from _pytest.fixtures import yield_fixture from _pytest.freeze_support import freeze_includes from _pytest.main import Session from _pytest.mark import MARK_GEN as mark From a03e076e892277261b73507d90cab4dc7b79ad6c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 3 Mar 2020 09:48:46 -0300 Subject: [PATCH 020/823] Update changelog/6285.feature.rst Co-Authored-By: Ran Benita --- changelog/6285.feature.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/6285.feature.rst b/changelog/6285.feature.rst index 74c69122402..bac353c86a5 100644 --- a/changelog/6285.feature.rst +++ b/changelog/6285.feature.rst @@ -1,2 +1,2 @@ -Use `pytest.FixtureLookupError` to catch exceptions when calling `request.getfixturevalue`. - +Exposed the `pytest.FixtureLookupError` exception which is raised by `request.getfixturevalue()` +(where `request` is a `FixtureRequest` fixture) when a fixture with the given name cannot be returned. From aac11e5e75b4aef295e857a2ae96e4438f28d6b9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 3 Mar 2020 10:05:19 -0300 Subject: [PATCH 021/823] Mention FixtureLookupError in getfixturevalue docs --- src/_pytest/fixtures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index cdd249d93a6..61849b762cc 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -482,6 +482,9 @@ def getfixturevalue(self, argname): But if you can only decide whether to use another fixture at test setup time, you may use this function to retrieve it inside a fixture or test function body. + + :raise pytest.FixtureLookupError: + If the given fixture could not be found. """ return self._get_active_fixturedef(argname).cached_result[0] From a42e85ed54804bc86ae1a65eeb91a6746b23f6a9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 3 Mar 2020 18:12:12 +0100 Subject: [PATCH 022/823] Fix documentation for _pytest.pytester.RunResult When using `(i)var` in the class docstring it would link `duration` to `_pytest.runner.TestReport.duration`. This moves the docstrings to the attributes properly. --- src/_pytest/pytester.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 9df3ed779d5..f25f8e10c5d 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -395,19 +395,7 @@ def _config_for_test(): class RunResult: - """The result of running a command. - - Attributes: - - :ivar ret: the return value - :ivar outlines: list of lines captured from stdout - :ivar errlines: list of lines captured from stderr - :ivar stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to - reconstruct stdout or the commonly used ``stdout.fnmatch_lines()`` - method - :ivar stderr: :py:class:`LineMatcher` of stderr - :ivar duration: duration in seconds - """ + """The result of running a command.""" def __init__( self, @@ -418,13 +406,23 @@ def __init__( ) -> None: try: self.ret = pytest.ExitCode(ret) # type: Union[int, ExitCode] + """the return value""" except ValueError: self.ret = ret self.outlines = outlines + """list of lines captured from stdout""" self.errlines = errlines + """list of lines captured from stderr""" self.stdout = LineMatcher(outlines) + """:class:`LineMatcher` of stdout. + + Use e.g. :func:`stdout.str() ` to reconstruct stdout, or the commonly used + :func:`stdout.fnmatch_lines() ` method. + """ self.stderr = LineMatcher(errlines) + """:class:`LineMatcher` of stderr""" self.duration = duration + """duration in seconds""" def __repr__(self) -> str: return ( From dc5219a9c008b3b77dfebd55b9924605c3a9a354 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 3 Mar 2020 21:15:06 +0100 Subject: [PATCH 023/823] Fix documentation for Config/InvocationParams --- src/_pytest/config/__init__.py | 35 +++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index f499a349afd..ec3fc6afe61 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -735,26 +735,19 @@ class Config: """ Access to configuration values, pluginmanager and plugin hooks. - :ivar PytestPluginManager pluginmanager: the plugin manager handles plugin registration and hook invocation. - - :ivar argparse.Namespace option: access to command line option as attributes. - - :ivar InvocationParams invocation_params: + :param PytestPluginManager pluginmanager: + :param InvocationParams invocation_params: Object containing the parameters regarding the ``pytest.main`` invocation. - - Contains the following read-only attributes: - - * ``args``: tuple of command-line arguments as passed to ``pytest.main()``. - * ``plugins``: list of extra plugins, might be None. - * ``dir``: directory where ``pytest.main()`` was invoked from. """ @attr.s(frozen=True) class InvocationParams: """Holds parameters passed during ``pytest.main()`` + The object attributes are read-only. + .. versionadded:: 5.1 .. note:: @@ -766,10 +759,18 @@ class InvocationParams: """ args = attr.ib(converter=tuple) + """tuple of command-line arguments as passed to ``pytest.main()``.""" plugins = attr.ib() + """list of extra plugins, might be `None`.""" dir = attr.ib(type=Path) - - def __init__(self, pluginmanager, *, invocation_params=None) -> None: + """directory where ``pytest.main()`` was invoked from.""" + + def __init__( + self, + pluginmanager: PytestPluginManager, + *, + invocation_params: InvocationParams = None + ) -> None: from .argparsing import Parser, FILE_OR_DIR if invocation_params is None: @@ -778,6 +779,10 @@ def __init__(self, pluginmanager, *, invocation_params=None) -> None: ) self.option = argparse.Namespace() + """access to command line option as attributes. + + :type: argparse.Namespace""" + self.invocation_params = invocation_params _a = FILE_OR_DIR @@ -786,6 +791,10 @@ def __init__(self, pluginmanager, *, invocation_params=None) -> None: processopt=self._processopt, ) self.pluginmanager = pluginmanager + """the plugin manager handles plugin registration and hook invocation. + + :type: PytestPluginManager""" + self.trace = self.pluginmanager.trace.root.get("config") self.hook = self.pluginmanager.hook self._inicache = {} # type: Dict[str, Any] From d161bedcee9b09dba53c66d12a60a3df8adb8e17 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 4 Mar 2020 09:23:31 -0300 Subject: [PATCH 024/823] Add an example of how to port the code --- doc/en/deprecations.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index b707caa1310..13d59bce2b7 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -45,6 +45,19 @@ This limitation in api surface intends to enable better/simpler refactoring of t This means that instead of :code:`MyItem(name="foo", parent=collector, obj=42)` one now has to invoke :code:`MyItem.from_parent(collector, name="foo")`. +Plugins that wish to support older versions of pytest and suppress the warning can use +`hasattr` to check if `from_parent` exists in that version: + +.. code-block:: python + + def pytest_pycollect_makeitem(collector, name, obj): + if hasattr(MyItem, "from_parent"): + item = MyItem.from_parent(collector, name="foo") + item.obj = 42 + return item + else: + return MyItem(name="foo", parent=collector, obj=42) + Note that ``from_parent`` should only be called with keyword arguments for the parameters. From 1a8d427e983270f5a865845628bb1dd2876bd6ad Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 5 Mar 2020 02:47:21 +0100 Subject: [PATCH 025/823] doc: src/_pytest/deprecated.py: links --- src/_pytest/deprecated.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 7e241ae1b39..28ca02550d4 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -5,7 +5,8 @@ Keeping it in a central location makes it easy to track what is deprecated and should be removed when the time comes. -All constants defined in this module should be either PytestWarning instances or UnformattedWarning +All constants defined in this module should be either instances of +:class:`PytestWarning`, or :class:`UnformattedWarning` in case of warnings which need to format their messages. """ from _pytest.warning_types import PytestDeprecationWarning From 77adb33ec613bb64e0a68d4bc9c9393c2dab08f5 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 5 Mar 2020 02:56:18 +0100 Subject: [PATCH 026/823] doc: use show-inheritance with warnings, revisit docstrings Revisits the docstring for `PytestExperimentalApiWarning` and `PytestUnhandledCoroutineWarning`. --- doc/en/warnings.rst | 9 ++++++ src/_pytest/warning_types.py | 56 +++++++++--------------------------- 2 files changed, 23 insertions(+), 42 deletions(-) diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 013564c2dfd..550e294efbe 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -416,19 +416,28 @@ features. The following warning types are used by pytest and are part of the public API: .. autoclass:: pytest.PytestWarning + :show-inheritance: .. autoclass:: pytest.PytestAssertRewriteWarning + :show-inheritance: .. autoclass:: pytest.PytestCacheWarning + :show-inheritance: .. autoclass:: pytest.PytestCollectionWarning + :show-inheritance: .. autoclass:: pytest.PytestConfigWarning + :show-inheritance: .. autoclass:: pytest.PytestDeprecationWarning + :show-inheritance: .. autoclass:: pytest.PytestExperimentalApiWarning + :show-inheritance: .. autoclass:: pytest.PytestUnhandledCoroutineWarning + :show-inheritance: .. autoclass:: pytest.PytestUnknownMarkWarning + :show-inheritance: diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 2e03c578c02..87ab72c2db8 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -11,71 +11,46 @@ class PytestWarning(UserWarning): - """ - Bases: :class:`UserWarning`. - - Base class for all warnings emitted by pytest. - """ + """Base class for all warnings emitted by pytest.""" __module__ = "pytest" class PytestAssertRewriteWarning(PytestWarning): - """ - Bases: :class:`PytestWarning`. - - Warning emitted by the pytest assert rewrite module. - """ + """Warning emitted by the pytest assert rewrite module.""" __module__ = "pytest" class PytestCacheWarning(PytestWarning): - """ - Bases: :class:`PytestWarning`. - - Warning emitted by the cache plugin in various situations. - """ + """Warning emitted by the cache plugin in various situations.""" __module__ = "pytest" class PytestConfigWarning(PytestWarning): - """ - Bases: :class:`PytestWarning`. - - Warning emitted for configuration issues. - """ + """Warning emitted for configuration issues.""" __module__ = "pytest" class PytestCollectionWarning(PytestWarning): - """ - Bases: :class:`PytestWarning`. - - Warning emitted when pytest is not able to collect a file or symbol in a module. - """ + """Warning emitted when pytest is not able to collect a file or symbol in a module.""" __module__ = "pytest" class PytestDeprecationWarning(PytestWarning, DeprecationWarning): - """ - Bases: :class:`pytest.PytestWarning`, :class:`DeprecationWarning`. - - Warning class for features that will be removed in a future version. - """ + """Warning class for features that will be removed in a future version.""" __module__ = "pytest" class PytestExperimentalApiWarning(PytestWarning, FutureWarning): - """ - Bases: :class:`pytest.PytestWarning`, :class:`FutureWarning`. + """Warning category used to denote experiments in pytest. - Warning category used to denote experiments in pytest. Use sparingly as the API might change or even be - removed completely in future version + Use sparingly as the API might change or even be removed completely in a + future version. """ __module__ = "pytest" @@ -90,22 +65,19 @@ def simple(cls, apiname: str) -> "PytestExperimentalApiWarning": class PytestUnhandledCoroutineWarning(PytestWarning): - """ - Bases: :class:`PytestWarning`. + """Warning emitted for an unhandled coroutine. - Warning emitted when pytest encounters a test function which is a coroutine, - but it was not handled by any async-aware plugin. Coroutine test functions - are not natively supported. + A coroutine was encountered when collecting test functions, but was not + handled by any async-aware plugin. + Coroutine test functions are not natively supported. """ __module__ = "pytest" class PytestUnknownMarkWarning(PytestWarning): - """ - Bases: :class:`PytestWarning`. + """Warning emitted on use of unknown markers. - Warning emitted on use of unknown markers. See https://docs.pytest.org/en/latest/mark.html for details. """ From b90f57d25c43f797f8afdb9f5a336dac5631c28a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 5 Mar 2020 03:13:28 +0100 Subject: [PATCH 027/823] Remove wrong/outdated doc with UnformattedWarning It was introduced in da6830f19 (added to `_pytest.deprecated`, but then moved to `_pytest.warning_types`). --- src/_pytest/warning_types.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 87ab72c2db8..e99de5c473f 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -90,8 +90,6 @@ class PytestUnknownMarkWarning(PytestWarning): @attr.s class UnformattedWarning(Generic[_W]): """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. - - Using this class avoids to keep all the warning types and messages in this module, avoiding misuse. """ category = attr.ib(type="Type[_W]") From c39a85e5f4efc9e8875802bb838f0ed91c6336ba Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 5 Mar 2020 03:15:14 +0100 Subject: [PATCH 028/823] doc: revisit UnformattedWarning --- src/_pytest/warning_types.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index e99de5c473f..ee437cc9746 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -89,7 +89,10 @@ class PytestUnknownMarkWarning(PytestWarning): @attr.s class UnformattedWarning(Generic[_W]): - """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. + """A warning meant to be formatted during runtime. + + This is used to hold warnings that need to format their message at runtime, + as opposed to a direct message. """ category = attr.ib(type="Type[_W]") From 9b32794391ae1b494c5082999bb69ee1a5038bcb Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 5 Mar 2020 05:53:42 +0100 Subject: [PATCH 029/823] intersphinx_mapping: add pluggy --- doc/en/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/en/conf.py b/doc/en/conf.py index 71f63712e11..cb0e846ada2 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -344,7 +344,10 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} +intersphinx_mapping = { + "pluggy": ("https://pluggy.readthedocs.io/en/latest", None), + "python": ("https://docs.python.org/3", None), +} def configure_logging(app: "sphinx.application.Sphinx") -> None: From a1ad6e31173b0e26dc68c5e90ce524ed209da312 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 5 Mar 2020 05:55:04 +0100 Subject: [PATCH 030/823] doc: fix/revisit _Result (hook wrappers) - it should not document the deprecated `result`; used the same as pluggy documents itself - add a "hookwrapper" label, that could be used by pluggy (currently it links to the section) - use pluggy's `hookwrappers` label for linking to its documentation --- doc/en/reference.rst | 5 ++++- doc/en/writing_plugins.rst | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 16bf3cf7938..b746172ea16 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -886,8 +886,11 @@ TestReport _Result ~~~~~~~ +Result used within :ref:`hook wrappers `. + .. autoclass:: pluggy.callers._Result - :members: +.. automethod:: pluggy.callers._Result.get_result +.. automethod:: pluggy.callers._Result.force_result Special Variables ----------------- diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index e914af40169..f590a12459c 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -508,6 +508,7 @@ call only executes until the first of N registered functions returns a non-None result which is then taken as result of the overall hook call. The remaining hook functions will not be called in this case. +.. _`hookwrapper`: hookwrapper: executing around other hooks ------------------------------------------------- @@ -552,7 +553,8 @@ 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. -For more information, consult the `pluggy documentation `_. +For more information, consult the +:ref:`pluggy documentation about hookwrappers `. Hook function ordering / call example From ffa2658971e29536c759ca52bffb2a6040e74ac4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 5 Mar 2020 05:57:43 +0100 Subject: [PATCH 031/823] doc: reports: count_towards_summary: is a property And therefore does not really `return`. It confused me that there was no `source` link in the docs, which is only there for functions. --- src/_pytest/reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 4fa465ea71c..0cd8380160b 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -131,7 +131,7 @@ def count_towards_summary(self): """ **Experimental** - Returns True if this report should be counted towards the totals shown at the end of the + ``True`` if this report should be counted towards the totals shown at the end of the test session: "1 passed, 1 failure, etc". .. note:: From 3865f77de3f40fc035e6365bfcf28f600d29e5e9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 5 Mar 2020 06:00:11 +0100 Subject: [PATCH 032/823] doc: TestReport: :show-inheritance: --- doc/en/reference.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index b746172ea16..cc70a0ed588 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -881,6 +881,7 @@ TestReport .. autoclass:: _pytest.runner.TestReport() :members: + :show-inheritance: :inherited-members: _Result From d9a462694414ff62ed1605b139808531ee6a708d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 5 Mar 2020 06:38:44 +0100 Subject: [PATCH 033/823] fixup! Fix documentation for Config/InvocationParams --- src/_pytest/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ec3fc6afe61..7468d2c31c8 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -769,7 +769,7 @@ def __init__( self, pluginmanager: PytestPluginManager, *, - invocation_params: InvocationParams = None + invocation_params: Optional[InvocationParams] = None ) -> None: from .argparsing import Parser, FILE_OR_DIR From 599bf075dbf1c1afb1a59b521dca52155c157488 Mon Sep 17 00:00:00 2001 From: gdhameeja Date: Tue, 3 Mar 2020 12:45:32 +0530 Subject: [PATCH 034/823] Check invalid operations for -k `KeywordMapping` returns a bool on lookup which when passed to eval fail on certain operations such as index access and attribute access. We catch all exceptions and raise a `UsageError`. --- src/_pytest/mark/legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/mark/legacy.py b/src/_pytest/mark/legacy.py index 766b8f9bd31..bf8004c1d53 100644 --- a/src/_pytest/mark/legacy.py +++ b/src/_pytest/mark/legacy.py @@ -106,5 +106,5 @@ def matchkeyword(colitem, keywordexpr): ) try: return eval(keywordexpr, {}, mapping) - except SyntaxError: + except Exception: raise UsageError("Wrong expression passed to '-k': {}".format(keywordexpr)) From 6954b3b0dcca48157b744d1c35cb9ac14146759b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 12 Mar 2020 14:56:57 +0200 Subject: [PATCH 035/823] Assume os.dup is always available The commit which added the checks for os.dup a15afb5e4879d68033a723129e6 suggests it was done for Jython. But pytest doesn't support Jython anymore (Jython is Python 2 only). Furthermore, it looks like the faulthandler plugin (bundled in pytest and enabled by default) uses os.dup() unprotected and there have not been any complaints. So seems better to just remove these checks, and only add if someone with a legitimate use case complains. --- changelog/6903.breaking.rst | 2 ++ src/_pytest/capture.py | 10 +------- testing/test_capture.py | 50 +------------------------------------ 3 files changed, 4 insertions(+), 58 deletions(-) create mode 100644 changelog/6903.breaking.rst diff --git a/changelog/6903.breaking.rst b/changelog/6903.breaking.rst new file mode 100644 index 00000000000..a074a4ffecb --- /dev/null +++ b/changelog/6903.breaking.rst @@ -0,0 +1,2 @@ +The ``os.dup()`` function is now assumed to exist. We are not aware of any +supported Python 3 implementations which do not provide it. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 5f29c5ca2f6..2af207e2166 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -34,7 +34,7 @@ def pytest_addoption(parser): group._addoption( "--capture", action="store", - default="fd" if hasattr(os, "dup") else "sys", + default="fd", metavar="method", choices=["fd", "sys", "no", "tee-sys"], help="per-test capturing method: one of fd|sys|no|tee-sys.", @@ -304,10 +304,6 @@ def capfd(request): calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. """ - if not hasattr(os, "dup"): - pytest.skip( - "capfd fixture needs os.dup function which is not available in this system" - ) capman = request.config.pluginmanager.getplugin("capturemanager") with capman._capturing_for_request(request) as fixture: yield fixture @@ -321,10 +317,6 @@ def capfdbinary(request): calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``byte`` objects. """ - if not hasattr(os, "dup"): - pytest.skip( - "capfdbinary fixture needs os.dup function which is not available in this system" - ) capman = request.config.pluginmanager.getplugin("capturemanager") with capman._capturing_for_request(request) as fixture: yield fixture diff --git a/testing/test_capture.py b/testing/test_capture.py index a3e558560a1..a2b100fdaf0 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -23,11 +23,6 @@ # pylib 1.4.20.dev2 (rev 13d9af95547e) -needsosdup = pytest.mark.skipif( - not hasattr(os, "dup"), reason="test needs os.dup, not available on this platform" -) - - def StdCaptureFD(out=True, err=True, in_=True): return capture.MultiCapture(out, err, in_, Capture=capture.FDCapture) @@ -41,22 +36,7 @@ def TeeStdCapture(out=True, err=True, in_=True): class TestCaptureManager: - def test_getmethod_default_no_fd(self, monkeypatch): - from _pytest.capture import pytest_addoption - from _pytest.config.argparsing import Parser - - parser = Parser() - pytest_addoption(parser) - default = parser._groups[0].options[0].default - assert default == "fd" if hasattr(os, "dup") else "sys" - parser = Parser() - monkeypatch.delattr(os, "dup", raising=False) - pytest_addoption(parser) - assert parser._groups[0].options[0].default == "sys" - - @pytest.mark.parametrize( - "method", ["no", "sys", pytest.param("fd", marks=needsosdup)] - ) + @pytest.mark.parametrize("method", ["no", "sys", "fd"]) def test_capturing_basic_api(self, method): capouter = StdCaptureFD() old = sys.stdout, sys.stderr, sys.stdin @@ -86,7 +66,6 @@ def test_capturing_basic_api(self, method): finally: capouter.stop_capturing() - @needsosdup def test_init_capturing(self): capouter = StdCaptureFD() try: @@ -512,7 +491,6 @@ def test_hello(cap{}): result = testdir.runpytest(p) result.stdout.fnmatch_lines(["xxx42xxx"]) - @needsosdup def test_stdfd_functional(self, testdir): reprec = testdir.inline_runsource( """\ @@ -526,7 +504,6 @@ def test_hello(capfd): ) reprec.assertoutcome(passed=1) - @needsosdup def test_capfdbinary(self, testdir): reprec = testdir.inline_runsource( """\ @@ -565,7 +542,6 @@ def test_hello(capsys, missingarg): result = testdir.runpytest(p) result.stdout.fnmatch_lines(["*test_partial_setup_failure*", "*1 error*"]) - @needsosdup def test_keyboardinterrupt_disables_capturing(self, testdir): p = testdir.makepyfile( """\ @@ -700,20 +676,6 @@ def pytest_runtest_setup(item): result.stdout.fnmatch_lines(["*ValueError(42)*", "*1 error*"]) -def test_fdfuncarg_skips_on_no_osdup(testdir): - testdir.makepyfile( - """ - import os - if hasattr(os, 'dup'): - del os.dup - def test_hello(capfd): - pass - """ - ) - result = testdir.runpytest_subprocess("--capture=no") - result.stdout.fnmatch_lines(["*1 skipped*"]) - - def test_capture_conftest_runtest_setup(testdir): testdir.makeconftest( """ @@ -865,7 +827,6 @@ def tmpfile(testdir) -> Generator[BinaryIO, None, None]: f.close() -@needsosdup def test_dupfile(tmpfile) -> None: flist = [] # type: List[TextIO] for i in range(5): @@ -924,8 +885,6 @@ def lsof_check(): class TestFDCapture: - pytestmark = needsosdup - def test_simple(self, tmpfile): fd = tmpfile.fileno() cap = capture.FDCapture(fd) @@ -1169,7 +1128,6 @@ def test_capturing_error_recursive(self): class TestStdCaptureFD(TestStdCapture): - pytestmark = needsosdup captureclass = staticmethod(StdCaptureFD) def test_simple_only_fd(self, testdir): @@ -1212,8 +1170,6 @@ def test_many(self, capfd): class TestStdCaptureFDinvalidFD: - pytestmark = needsosdup - def test_stdcapture_fd_invalid_fd(self, testdir): testdir.makepyfile( """ @@ -1269,7 +1225,6 @@ def test_capsys_results_accessible_by_attribute(capsys): assert capture_result.err == "eggs" -@needsosdup @pytest.mark.parametrize("use", [True, False]) def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): if not use: @@ -1285,7 +1240,6 @@ def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): assert capfile2 == capfile -@needsosdup def test_close_and_capture_again(testdir): testdir.makepyfile( """ @@ -1310,8 +1264,6 @@ def test_capture_again(): @pytest.mark.parametrize("method", ["SysCapture", "FDCapture", "TeeSysCapture"]) def test_capturing_and_logging_fundamentals(testdir, method): - if method == "StdCaptureFD" and not hasattr(os, "dup"): - pytest.skip("need os.dup") # here we check a fundamental feature p = testdir.makepyfile( """ From 0c58ed2cc02bb2470c49e6151831d89a5c9d9f94 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 12 Mar 2020 20:47:29 -0300 Subject: [PATCH 036/823] Handle unknown stats in pytest_report_teststatus hook Noticed that the pytest_report_teststatus of reportlog was not properly handling unknown statuses while taking a look at: https://github.com/pytest-dev/pytest-rerunfailures/issues/103 --- changelog/6910.bugfix.rst | 1 + src/_pytest/resultlog.py | 4 ++-- testing/test_resultlog.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 changelog/6910.bugfix.rst diff --git a/changelog/6910.bugfix.rst b/changelog/6910.bugfix.rst new file mode 100644 index 00000000000..713824998d1 --- /dev/null +++ b/changelog/6910.bugfix.rst @@ -0,0 +1 @@ +Fix crash when plugins return an unknown stats while using the ``--reportlog`` option. diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index 6269c16f2be..3cfa9e0e96a 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -77,10 +77,10 @@ def pytest_runtest_logreport(self, report): longrepr = "" elif report.passed: longrepr = "" - elif report.failed: - longrepr = str(report.longrepr) elif report.skipped: longrepr = str(report.longrepr[2]) + else: + longrepr = str(report.longrepr) self.log_outcome(report, code, longrepr) def pytest_collectreport(self, report): diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index e0a02de8029..bad575e3d13 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -193,6 +193,42 @@ def test_no_resultlog_on_slaves(testdir): assert resultlog_key not in config._store +def test_unknown_teststatus(testdir): + """Ensure resultlog correctly handles unknown status from pytest_report_teststatus + + Inspired on pytest-rerunfailures. + """ + testdir.makepyfile( + """ + def test(): + assert 0 + """ + ) + testdir.makeconftest( + """ + import pytest + + def pytest_report_teststatus(report): + if report.outcome == 'rerun': + return "rerun", "r", "RERUN" + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_makereport(): + res = yield + report = res.get_result() + if report.when == "call": + report.outcome = 'rerun' + """ + ) + result = testdir.runpytest("--resultlog=result.log") + result.stdout.fnmatch_lines( + ["test_unknown_teststatus.py r *[[]100%[]]", "* 1 rerun *"] + ) + + lines = testdir.tmpdir.join("result.log").readlines(cr=0) + assert lines[0] == "r test_unknown_teststatus.py::test" + + def test_failure_issue380(testdir): testdir.makeconftest( """ From b1b8ea765ec1b40872fab9bbe0f51c859557ad12 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Mar 2020 10:27:07 -0300 Subject: [PATCH 037/823] Skip link checks when doing releases through the bot Unfortunately this is really getting in the way of the releases not because of broken links, but because it is very flaky. Related: #6894 --- scripts/release-on-comment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/release-on-comment.py b/scripts/release-on-comment.py index bd4986eaa8f..90235fd55c4 100644 --- a/scripts/release-on-comment.py +++ b/scripts/release-on-comment.py @@ -126,7 +126,9 @@ def trigger_release(payload_path: Path, token: str) -> None: print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.") - check_call([sys.executable, "scripts/release.py", version]) + check_call( + [sys.executable, "scripts/release.py", version, "--skip-check-links"] + ) oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git" check_call(["git", "push", oauth_url, f"HEAD:{release_branch}", "--force"]) From 010e711971bdc5aee24ce1dc5de70ca4db62b3d9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Mar 2020 10:18:06 -0300 Subject: [PATCH 038/823] Merge pull request #6914 from nicoddemus/revert-6330 Revert "[parametrize] enforce explicit argnames declaration (#6330)" --- changelog/6909.bugfix.rst | 3 ++ doc/en/example/parametrize.rst | 3 -- src/_pytest/fixtures.py | 16 ++++------- src/_pytest/python.py | 35 ----------------------- testing/python/collect.py | 2 +- testing/python/metafunc.py | 51 ---------------------------------- 6 files changed, 9 insertions(+), 101 deletions(-) create mode 100644 changelog/6909.bugfix.rst diff --git a/changelog/6909.bugfix.rst b/changelog/6909.bugfix.rst new file mode 100644 index 00000000000..32edc4974c2 --- /dev/null +++ b/changelog/6909.bugfix.rst @@ -0,0 +1,3 @@ +Revert the change introduced by `#6330 `_, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature. + +The intention of the original change was to remove what was expected to be an unintended/surprising behavior, but it turns out many people relied on it, so the restriction has been reverted. diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index df558d1bae6..14e6537adaa 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -402,9 +402,6 @@ The result of this test will be successful: .. regendoc:wipe -Note, that each argument in `parametrize` list should be explicitly declared in corresponding -python test function or via `indirect`. - Parametrizing test methods through per-class configuration -------------------------------------------------------------- diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 081e95a6db8..22964770d22 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1,5 +1,6 @@ import functools import inspect +import itertools import sys import warnings from collections import defaultdict @@ -1277,8 +1278,10 @@ def getfixtureinfo(self, node, func, cls, funcargs=True): else: argnames = () - usefixtures = get_use_fixtures_for_node(node) - initialnames = usefixtures + argnames + usefixtures = itertools.chain.from_iterable( + mark.args for mark in node.iter_markers(name="usefixtures") + ) + initialnames = tuple(usefixtures) + argnames fm = node.session._fixturemanager initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure( initialnames, node, ignore_args=self._get_direct_parametrize_args(node) @@ -1475,12 +1478,3 @@ def _matchfactories(self, fixturedefs, nodeid): for fixturedef in fixturedefs: if nodes.ischildnode(fixturedef.baseid, nodeid): yield fixturedef - - -def get_use_fixtures_for_node(node) -> Tuple[str, ...]: - """Returns the names of all the usefixtures() marks on the given node""" - return tuple( - str(name) - for mark in node.iter_markers(name="usefixtures") - for name in mark.args - ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e260761794d..6805a72fbf7 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -936,8 +936,6 @@ def parametrize( arg_values_types = self._resolve_arg_value_types(argnames, indirect) - self._validate_explicit_parameters(argnames, indirect) - # Use any already (possibly) generated ids with parametrize Marks. if _param_mark and _param_mark._param_ids_from: generated_ids = _param_mark._param_ids_from._param_ids_generated @@ -1110,39 +1108,6 @@ def _validate_if_using_arg_names( pytrace=False, ) - def _validate_explicit_parameters( - self, - argnames: typing.Sequence[str], - indirect: Union[bool, typing.Sequence[str]], - ) -> None: - """ - The argnames in *parametrize* should either be declared explicitly via - indirect list or in the function signature - - :param List[str] argnames: list of argument names passed to ``parametrize()``. - :param indirect: same ``indirect`` parameter of ``parametrize()``. - :raise ValueError: if validation fails - """ - if isinstance(indirect, bool): - parametrized_argnames = [] if indirect else argnames - else: - parametrized_argnames = [arg for arg in argnames if arg not in indirect] - - if not parametrized_argnames: - return - - funcargnames = _pytest.compat.getfuncargnames(self.function) - usefixtures = fixtures.get_use_fixtures_for_node(self.definition) - - for arg in parametrized_argnames: - if arg not in funcargnames and arg not in usefixtures: - func_name = self.function.__name__ - msg = ( - 'In function "{func_name}":\n' - 'Parameter "{arg}" should be declared explicitly via indirect or in function itself' - ).format(func_name=func_name, arg=arg) - fail(msg, pytrace=False) - def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): """Find the most appropriate scope for a parametrized call based on its arguments. diff --git a/testing/python/collect.py b/testing/python/collect.py index 047d5f18e15..496a22b0504 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -463,7 +463,7 @@ def fix3(): return '3' @pytest.mark.parametrize('fix2', ['2']) - def test_it(fix1, fix2): + def test_it(fix1): assert fix1 == '21' assert not fix3_instantiated """ diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 4e657727c32..4d41910982b 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -36,9 +36,6 @@ def __init__(self, names): class DefinitionMock(python.FunctionDefinition): obj = attr.ib() - def listchain(self): - return [] - names = fixtures.getfuncargnames(func) fixtureinfo = FuncFixtureInfoMock(names) # type: Any definition = DefinitionMock._create(func) # type: Any @@ -1902,51 +1899,3 @@ def test_converted_to_str(a, b): "*= 6 passed in *", ] ) - - def test_parametrize_explicit_parameters_func(self, testdir: Testdir) -> None: - testdir.makepyfile( - """ - import pytest - - - @pytest.fixture - def fixture(arg): - return arg - - @pytest.mark.parametrize("arg", ["baz"]) - def test_without_arg(fixture): - assert "baz" == fixture - """ - ) - result = testdir.runpytest() - result.assert_outcomes(error=1) - result.stdout.fnmatch_lines( - [ - '*In function "test_without_arg"*', - '*Parameter "arg" should be declared explicitly via indirect or in function itself*', - ] - ) - - def test_parametrize_explicit_parameters_method(self, testdir: Testdir) -> None: - testdir.makepyfile( - """ - import pytest - - class Test: - @pytest.fixture - def test_fixture(self, argument): - return argument - - @pytest.mark.parametrize("argument", ["foobar"]) - def test_without_argument(self, test_fixture): - assert "foobar" == test_fixture - """ - ) - result = testdir.runpytest() - result.assert_outcomes(error=1) - result.stdout.fnmatch_lines( - [ - '*In function "test_without_argument"*', - '*Parameter "argument" should be declared explicitly via indirect or in function itself*', - ] - ) From 68d4b17a5f2fecc06806adaf335a6151d9df62d4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Mar 2020 11:13:05 -0300 Subject: [PATCH 039/823] Cherry pick CHANGELOG from 5.4.1 --- doc/en/announce/index.rst | 1 + doc/en/announce/release-5.4.1.rst | 18 ++++++++++++++++++ doc/en/changelog.rst | 22 ++++++++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 doc/en/announce/release-5.4.1.rst diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 3bb3b0b9ec9..56e3172dd65 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.4.1 release-5.4.0 release-5.3.5 release-5.3.4 diff --git a/doc/en/announce/release-5.4.1.rst b/doc/en/announce/release-5.4.1.rst new file mode 100644 index 00000000000..413bf7d2bac --- /dev/null +++ b/doc/en/announce/release-5.4.1.rst @@ -0,0 +1,18 @@ +pytest-5.4.1 +======================================= + +pytest 5.4.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Bruno Oliveira + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 8f6d140c1be..ac3b4ad85ef 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,20 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.4.1 (2020-03-13) +========================= + +Bug Fixes +--------- + +- `#6909 `_: Revert the change introduced by `#6330 `_, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature. + + The intention of the original change was to remove what was expected to be an unintended/surprising behavior, but it turns out many people relied on it, so the restriction has been reverted. + + +- `#6910 `_: Fix crash when plugins return an unknown stats while using the ``--reportlog`` option. + + pytest 5.4.0 (2020-03-12) ========================= @@ -63,10 +77,10 @@ Breaking Changes Deprecations ------------ -- `#3238 `_: Option ``--no-print-logs`` is deprecated and meant to be removed in a future release. If you use ``--no-print-logs``, please try out ``--show-capture`` and - provide feedback. - - ``--show-capture`` command-line option was added in ``pytest 3.5.0`` and allows to specify how to +- `#3238 `_: Option ``--no-print-logs`` is deprecated and meant to be removed in a future release. If you use ``--no-print-logs``, please try out ``--show-capture`` and + provide feedback. + + ``--show-capture`` command-line option was added in ``pytest 3.5.0`` and allows to specify how to display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default). From 29e4cb5d45f44379aba948c2cd791b3b97210e31 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 7 Mar 2020 18:38:22 +0200 Subject: [PATCH 040/823] Remove safe_text_dupfile() and simplify EncodedFile I tried to understand what the `safe_text_dupfile()` function and `EncodedFile` class do. Outside tests, `EncodedFile` is only used by `safe_text_dupfile`, and `safe_text_dupfile` is only used by `FDCaptureBinary.__init__()`. I then started to eliminate always-true conditions based on the single call site, and in the end nothing was left except of a couple workarounds that are still needed. --- src/_pytest/capture.py | 66 ++++++++++-------------------------- testing/test_capture.py | 75 ++++++----------------------------------- 2 files changed, 28 insertions(+), 113 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 2af207e2166..a64c72b5a7d 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -9,9 +9,7 @@ import sys from io import UnsupportedOperation from tempfile import TemporaryFile -from typing import BinaryIO from typing import Generator -from typing import Iterable from typing import Optional import pytest @@ -382,54 +380,21 @@ def disabled(self): yield -def safe_text_dupfile(f, mode, default_encoding="UTF8"): - """ return an open text file object that's a duplicate of f on the - FD-level if possible. - """ - encoding = getattr(f, "encoding", None) - try: - fd = f.fileno() - except Exception: - if "b" not in getattr(f, "mode", "") and hasattr(f, "encoding"): - # we seem to have a text stream, let's just use it - return f - else: - newfd = os.dup(fd) - if "b" not in mode: - mode += "b" - f = os.fdopen(newfd, mode, 0) # no buffering - return EncodedFile(f, encoding or default_encoding) - - -class EncodedFile: - errors = "strict" # possibly needed by py3 code (issue555) - - def __init__(self, buffer: BinaryIO, encoding: str) -> None: - self.buffer = buffer - self.encoding = encoding - - def write(self, s: str) -> int: - if not isinstance(s, str): - raise TypeError( - "write() argument must be str, not {}".format(type(s).__name__) - ) - return self.buffer.write(s.encode(self.encoding, "replace")) - - def writelines(self, lines: Iterable[str]) -> None: - self.buffer.writelines(x.encode(self.encoding, "replace") for x in lines) +class EncodedFile(io.TextIOWrapper): + __slots__ = () @property def name(self) -> str: - """Ensure that file.name is a string.""" + # Ensure that file.name is a string. Workaround for a Python bug + # fixed in >=3.7.4: https://bugs.python.org/issue36015 return repr(self.buffer) @property def mode(self) -> str: + # TextIOWrapper doesn't expose a mode, but at least some of our + # tests check it. return self.buffer.mode.replace("b", "") - def __getattr__(self, name): - return getattr(object.__getattribute__(self, "buffer"), name) - CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"]) @@ -544,9 +509,12 @@ def __init__(self, targetfd, tmpfile=None): self.syscapture = SysCapture(targetfd) else: if tmpfile is None: - f = TemporaryFile() - with f: - tmpfile = safe_text_dupfile(f, mode="wb+") + tmpfile = EncodedFile( + TemporaryFile(buffering=0), + encoding="utf-8", + errors="replace", + write_through=True, + ) if targetfd in patchsysdict: self.syscapture = SysCapture(targetfd, tmpfile) else: @@ -575,7 +543,7 @@ def _start(self): def snap(self): self.tmpfile.seek(0) - res = self.tmpfile.read() + res = self.tmpfile.buffer.read() self.tmpfile.seek(0) self.tmpfile.truncate() return res @@ -617,10 +585,10 @@ class FDCapture(FDCaptureBinary): EMPTY_BUFFER = str() # type: ignore def snap(self): - res = super().snap() - enc = getattr(self.tmpfile, "encoding", None) - if enc and isinstance(res, bytes): - res = str(res, enc, "replace") + self.tmpfile.seek(0) + res = self.tmpfile.read() + self.tmpfile.seek(0) + self.tmpfile.truncate() return res diff --git a/testing/test_capture.py b/testing/test_capture.py index a2b100fdaf0..65246151537 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1,16 +1,12 @@ import contextlib import io import os -import pickle import subprocess import sys import textwrap -from io import StringIO from io import UnsupportedOperation from typing import BinaryIO from typing import Generator -from typing import List -from typing import TextIO import pytest from _pytest import capture @@ -827,48 +823,6 @@ def tmpfile(testdir) -> Generator[BinaryIO, None, None]: f.close() -def test_dupfile(tmpfile) -> None: - flist = [] # type: List[TextIO] - for i in range(5): - nf = capture.safe_text_dupfile(tmpfile, "wb") - assert nf != tmpfile - assert nf.fileno() != tmpfile.fileno() - assert nf not in flist - print(i, end="", file=nf) - flist.append(nf) - - fname_open = flist[0].name - assert fname_open == repr(flist[0].buffer) - - for i in range(5): - f = flist[i] - f.close() - fname_closed = flist[0].name - assert fname_closed == repr(flist[0].buffer) - assert fname_closed != fname_open - tmpfile.seek(0) - s = tmpfile.read() - assert "01234" in repr(s) - tmpfile.close() - assert fname_closed == repr(flist[0].buffer) - - -def test_dupfile_on_bytesio(): - bio = io.BytesIO() - f = capture.safe_text_dupfile(bio, "wb") - f.write("hello") - assert bio.getvalue() == b"hello" - assert "BytesIO object" in f.name - - -def test_dupfile_on_textio(): - sio = StringIO() - f = capture.safe_text_dupfile(sio, "wb") - f.write("hello") - assert sio.getvalue() == "hello" - assert not hasattr(f, "name") - - @contextlib.contextmanager def lsof_check(): pid = os.getpid() @@ -1307,8 +1261,8 @@ def test_error_attribute_issue555(testdir): """ import sys def test_capattr(): - assert sys.stdout.errors == "strict" - assert sys.stderr.errors == "strict" + assert sys.stdout.errors == "replace" + assert sys.stderr.errors == "replace" """ ) reprec = testdir.inline_run() @@ -1383,15 +1337,6 @@ def test_spam_in_thread(): result.stdout.no_fnmatch_line("*IOError*") -def test_pickling_and_unpickling_encoded_file(): - # See https://bitbucket.org/pytest-dev/pytest/pull-request/194 - # pickle.loads() raises infinite recursion if - # EncodedFile.__getattr__ is not implemented properly - ef = capture.EncodedFile(None, None) - ef_as_str = pickle.dumps(ef) - pickle.loads(ef_as_str) - - def test_global_capture_with_live_logging(testdir): # Issue 3819 # capture should work with live cli logging @@ -1497,8 +1442,9 @@ def test_fails(): result_with_capture = testdir.runpytest(str(p)) assert result_with_capture.ret == result_without_capture.ret - result_with_capture.stdout.fnmatch_lines( - ["E * TypeError: write() argument must be str, not bytes"] + out = result_with_capture.stdout.str() + assert ("TypeError: write() argument must be str, not bytes" in out) or ( + "TypeError: unicode argument expected, got 'bytes'" in out ) @@ -1508,12 +1454,13 @@ def test_stderr_write_returns_len(capsys): def test_encodedfile_writelines(tmpfile: BinaryIO) -> None: - ef = capture.EncodedFile(tmpfile, "utf-8") - with pytest.raises(AttributeError): - ef.writelines([b"line1", b"line2"]) # type: ignore[list-item] # noqa: F821 - assert ef.writelines(["line1", "line2"]) is None # type: ignore[func-returns-value] # noqa: F821 + ef = capture.EncodedFile(tmpfile, encoding="utf-8") + with pytest.raises(TypeError): + ef.writelines([b"line1", b"line2"]) + assert ef.writelines(["line3", "line4"]) is None # type: ignore[func-returns-value] # noqa: F821 + ef.flush() tmpfile.seek(0) - assert tmpfile.read() == b"line1line2" + assert tmpfile.read() == b"line3line4" tmpfile.close() with pytest.raises(ValueError): ef.read() From 2d897ad39fd59ce4062b66736c0f4c72bf59773c Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Sat, 14 Mar 2020 20:01:50 -0400 Subject: [PATCH 041/823] Fix reference to the cache fixture --- doc/en/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 731037b0d3b..669b8272c3c 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -284,7 +284,7 @@ For more details, consult the full :ref:`fixtures docs `. :decorator: -.. fixture:: config.cache +.. fixture:: cache config.cache ~~~~~~~~~~~~ From 1fda86119055f7a5db8a30d867b26e802f6067ab Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 8 Mar 2020 02:15:15 +0100 Subject: [PATCH 042/823] Fix crash when printing while capsysbinary is active Previously, writing to sys.stdout/stderr in text-mode (e.g. `print('foo')`) while a `capsysbinary` fixture is active, would crash with: /usr/lib/python3.7/contextlib.py:119: in __exit__ next(self.gen) E TypeError: write() argument must be str, not bytes This is due to some confusion in the types. The relevant functions are `snap()` and `writeorg()`. The function `snap()` returns what was captured, and the return type should be `bytes` for the binary captures and `str` for the regular ones. The `snap()` return value is eventually passed to `writeorg()` to be written to the original file, so it's input type should correspond to `snap()`. But this was incorrect for `SysCaptureBinary`, which handled it like `str`. To fix this, be explicit in the `snap()` and `writeorg()` implementations, also of the other Capture types. We can't add type annotations yet, because the current inheritance scheme breaks Liskov Substitution and mypy would complain. To be refactored later. Fixes: https://github.com/pytest-dev/pytest/issues/6871 Co-authored-by: Ran Benita (some modifications & commit message) --- changelog/6871.bugfix.rst | 1 + src/_pytest/capture.py | 14 +++++++++++--- testing/test_capture.py | 36 +++++++++++++++++++++++++++++------- 3 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 changelog/6871.bugfix.rst diff --git a/changelog/6871.bugfix.rst b/changelog/6871.bugfix.rst new file mode 100644 index 00000000000..fe69c750915 --- /dev/null +++ b/changelog/6871.bugfix.rst @@ -0,0 +1 @@ +Fix crash with captured output when using the :fixture:`capsysbinary fixture `. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index a64c72b5a7d..90de1d9fce7 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -570,8 +570,6 @@ def resume(self): def writeorg(self, data): """ write to original file descriptor. """ - if isinstance(data, str): - data = data.encode("utf8") # XXX use encoding of original stream os.write(self.targetfd_save, data) @@ -591,6 +589,11 @@ def snap(self): self.tmpfile.truncate() return res + def writeorg(self, data): + """ write to original file descriptor. """ + data = data.encode("utf-8") # XXX use encoding of original stream + os.write(self.targetfd_save, data) + class SysCaptureBinary: @@ -642,8 +645,9 @@ def resume(self): self._state = "resumed" def writeorg(self, data): - self._old.write(data) self._old.flush() + self._old.buffer.write(data) + self._old.buffer.flush() class SysCapture(SysCaptureBinary): @@ -655,6 +659,10 @@ def snap(self): self.tmpfile.truncate() return res + def writeorg(self, data): + self._old.write(data) + self._old.flush() + class TeeSysCapture(SysCapture): def __init__(self, fd, tmpfile=None): diff --git a/testing/test_capture.py b/testing/test_capture.py index 65246151537..8b6e5967576 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -515,18 +515,40 @@ def test_hello(capfdbinary): reprec.assertoutcome(passed=1) def test_capsysbinary(self, testdir): - reprec = testdir.inline_runsource( - """\ + p1 = testdir.makepyfile( + r""" def test_hello(capsysbinary): import sys - # some likely un-decodable bytes - sys.stdout.buffer.write(b'\\xfe\\x98\\x20') + + sys.stdout.buffer.write(b'hello') + + # Some likely un-decodable bytes. + sys.stdout.buffer.write(b'\xfe\x98\x20') + + sys.stdout.buffer.flush() + + # Ensure writing in text mode still works and is captured. + # https://github.com/pytest-dev/pytest/issues/6871 + print("world", flush=True) + out, err = capsysbinary.readouterr() - assert out == b'\\xfe\\x98\\x20' + assert out == b'hello\xfe\x98\x20world\n' assert err == b'' + + print("stdout after") + print("stderr after", file=sys.stderr) """ ) - reprec.assertoutcome(passed=1) + result = testdir.runpytest(str(p1), "-rA") + result.stdout.fnmatch_lines( + [ + "*- Captured stdout call -*", + "stdout after", + "*- Captured stderr call -*", + "stderr after", + "*= 1 passed in *", + ] + ) def test_partial_setup_failure(self, testdir): p = testdir.makepyfile( @@ -890,7 +912,7 @@ def test_writeorg(self, tmpfile): cap.start() tmpfile.write(data1) tmpfile.flush() - cap.writeorg(data2) + cap.writeorg(data2.decode("ascii")) scap = cap.snap() cap.done() assert scap == data1.decode("ascii") From c6e530990f369fd4172d880e298d9545cca98c23 Mon Sep 17 00:00:00 2001 From: Mattwmaster58 Date: Fri, 20 Mar 2020 22:39:18 -0600 Subject: [PATCH 043/823] update available plugin count 315+ -> 815+ --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 864467ea210..0c05c9e33a7 100644 --- a/README.rst +++ b/README.rst @@ -91,7 +91,7 @@ Features - Python 3.5+ and PyPy3; -- Rich plugin architecture, with over 315+ `external plugins `_ and thriving community; +- Rich plugin architecture, with over 850+ `external plugins `_ and thriving community; Documentation From 2cc3227f6a36a0628b84301043ecf887973acb47 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 21 Mar 2020 15:59:54 +0200 Subject: [PATCH 044/823] ci: twisted and oldattrs tox envs are now incompatible, don't run them together twisted started to use `attr.s(eq)` argument which was added recently, so it fails with oldattrs. One of the CI jobs ran twisted and oldattrs together, so it started to fail. Move the twisted code to be covered by another job, and remove it from the job with the oldattrs. --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a8a9c527b38..80317f1c15c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,7 +62,7 @@ jobs: - name: "windows-py37" python: "3.7" os: windows-latest - tox_env: "py37-twisted-numpy" + tox_env: "py37-numpy" - name: "windows-py37-pluggy" python: "3.7" os: windows-latest @@ -70,7 +70,7 @@ jobs: - name: "windows-py38" python: "3.8" os: windows-latest - tox_env: "py38" + tox_env: "py38-twisted" use_coverage: true - name: "ubuntu-py35" @@ -84,7 +84,7 @@ jobs: - name: "ubuntu-py37" python: "3.7" os: ubuntu-latest - tox_env: "py37-lsof-numpy-oldattrs-pexpect-twisted" + tox_env: "py37-lsof-numpy-oldattrs-pexpect" use_coverage: true - name: "ubuntu-py37-pluggy" python: "3.7" From 817537523c16035ebecc41c5a6902d4d10d2b04d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 15 Mar 2020 22:49:14 +0200 Subject: [PATCH 045/823] Upgrade mypy 0.761 -> 0.770 https://mypy-lang.blogspot.com/2020/03/mypy-0770-released.html --- .pre-commit-config.yaml | 2 +- setup.py | 2 +- src/_pytest/python_api.py | 2 +- src/_pytest/recwarn.py | 10 +++++----- src/_pytest/reports.py | 2 +- src/_pytest/terminal.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1fd3e382754..8c47b557693 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: - id: pyupgrade args: [--py3-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.761 # NOTE: keep this in sync with setup.py. + rev: v0.770 # NOTE: keep this in sync with setup.py. hooks: - id: mypy files: ^(src/|testing/) diff --git a/setup.py b/setup.py index 892b55aed64..6ebfd67fbf5 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def main(): "xmlschema", ], "checkqa-mypy": [ - "mypy==v0.761", # keep this in sync with .pre-commit-config.yaml. + "mypy==v0.770", # keep this in sync with .pre-commit-config.yaml. ], }, install_requires=INSTALL_REQUIRES, diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index df97181f4fc..7f52778b9b0 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -678,7 +678,7 @@ def raises( # noqa: F811 """ __tracebackhide__ = True for exc in filterfalse( - inspect.isclass, always_iterable(expected_exception, BASE_TYPE) # type: ignore[arg-type] # noqa: F821 + inspect.isclass, always_iterable(expected_exception, BASE_TYPE) ): msg = "exceptions must be derived from BaseException, not %s" raise TypeError(msg % type(exc)) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index c57c94b1cb1..58b6fbab949 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -139,18 +139,18 @@ class WarningsRecorder(warnings.catch_warnings): def __init__(self): super().__init__(record=True) self._entered = False - self._list = [] # type: List[warnings._Record] + self._list = [] # type: List[warnings.WarningMessage] @property - def list(self) -> List["warnings._Record"]: + def list(self) -> List["warnings.WarningMessage"]: """The list of recorded warnings.""" return self._list - def __getitem__(self, i: int) -> "warnings._Record": + def __getitem__(self, i: int) -> "warnings.WarningMessage": """Get a recorded warning by index.""" return self._list[i] - def __iter__(self) -> Iterator["warnings._Record"]: + def __iter__(self) -> Iterator["warnings.WarningMessage"]: """Iterate through the recorded warnings.""" return iter(self._list) @@ -158,7 +158,7 @@ def __len__(self) -> int: """The number of recorded warnings.""" return len(self._list) - def pop(self, cls: "Type[Warning]" = Warning) -> "warnings._Record": + def pop(self, cls: "Type[Warning]" = Warning) -> "warnings.WarningMessage": """Pop the first recorded warning, raise exception if not exists.""" for i, w in enumerate(self._list): if issubclass(w.category, cls): diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 4fa465ea71c..25d8bf28b37 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -55,7 +55,7 @@ def __getattr__(self, key: str) -> Any: def toterminal(self, out) -> None: if hasattr(self, "node"): - out.line(getslaveinfoline(self.node)) # type: ignore + out.line(getslaveinfoline(self.node)) longrepr = self.longrepr if longrepr is None: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 7127ac74bcd..b0a2d253056 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -480,7 +480,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: self._write_progress_information_filling_space() else: self.ensure_newline() - self._tw.write("[%s]" % rep.node.gateway.id) # type: ignore + self._tw.write("[%s]" % rep.node.gateway.id) if self._show_progress_info: self._tw.write( self._get_progress_information_message() + " ", cyan=True From 27341d17fa5add72e514a00a74c44ae4d66be876 Mon Sep 17 00:00:00 2001 From: Lewis Belcher Date: Mon, 23 Mar 2020 09:02:06 +0100 Subject: [PATCH 046/823] Update fixture.rst Fix up some mangled wording. --- doc/en/fixture.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index fe69109a749..925a4b55982 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -415,7 +415,7 @@ Order: Higher-scoped fixtures are instantiated first -Within a function request for features, fixture of higher-scopes (such as ``session``) are instantiated first than +Within a function request for fixtures, those of higher-scopes (such as ``session``) are instantiated before lower-scoped fixtures (such as ``function`` or ``class``). The relative order of fixtures of same scope follows the declared order in the test function and honours dependencies between fixtures. Autouse fixtures will be instantiated before explicitly used fixtures. From 37cbab689967b050092e328cc622dda5e91dc763 Mon Sep 17 00:00:00 2001 From: "Curt J. Sampson" Date: Wed, 25 Mar 2020 21:31:55 +0900 Subject: [PATCH 047/823] CONTRIBUTING: Grammatical clarification and minor typo fixes The main unclear part was that "to contribute changes" read in two different ways; I've reworded it so it reads only one way. --- CONTRIBUTING.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 050c7082c57..d5137d9787a 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -2,7 +2,7 @@ Contribution getting started ============================ -Contributions are highly welcomed and appreciated. Every little help counts, +Contributions are highly welcomed and appreciated. Every little bit of help counts, so do not hesitate! .. contents:: @@ -86,9 +86,8 @@ without using a local copy. This can be convenient for small fixes. $ tox -e docs - The built documentation should be available in ``doc/en/_build/html``. - - Where 'en' refers to the documentation language. + The built documentation should be available in ``doc/en/_build/html``, + where 'en' refers to the documentation language. .. _submitplugin: @@ -130,7 +129,7 @@ the following: - an issue tracker for bug reports and enhancement requests. -- a `changelog `_ +- a `changelog `_. If no contributor strongly objects and two agree, the repository can then be transferred to the ``pytest-dev`` organisation. @@ -338,7 +337,7 @@ Joining the Development Team Anyone who has successfully seen through a pull request which did not require any extra work from the development team to merge will themselves gain commit access if they so wish (if we forget to ask please send a friendly -reminder). This does not mean your workflow to contribute changes, +reminder). This does not mean there is any change in your contribution workflow: everyone goes through the same pull-request-and-review process and no-one merges their own pull requests unless already approved. It does however mean you can participate in the development process more fully since you can merge From e651562271a03f1691165793f0935511fe98b227 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 27 Mar 2020 02:24:00 +0100 Subject: [PATCH 048/823] test_warnings: clean up usage of pyfile_with_warnings (#6799) Remove it where not used / overwritten, and use its reference otherwise, which makes it clear that it is used actually. --- testing/test_warnings.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 5387c8d4423..fba2c00f9c8 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -2,19 +2,28 @@ import warnings import pytest +from _pytest.fixtures import FixtureRequest +from _pytest.pytester import Testdir WARNINGS_SUMMARY_HEADER = "warnings summary" @pytest.fixture -def pyfile_with_warnings(testdir, request): +def pyfile_with_warnings(testdir: Testdir, request: FixtureRequest) -> str: """ Create a test file which calls a function in a module which generates warnings. """ testdir.syspathinsert() test_name = request.function.__name__ module_name = test_name.lstrip("test_") + "_module" - testdir.makepyfile( + test_file = testdir.makepyfile( + """ + import {module_name} + def test_func(): + assert {module_name}.foo() == 1 + """.format( + module_name=module_name + ), **{ module_name: """ import warnings @@ -22,16 +31,10 @@ def foo(): warnings.warn(UserWarning("user warning")) warnings.warn(RuntimeWarning("runtime warning")) return 1 - """, - test_name: """ - import {module_name} - def test_func(): - assert {module_name}.foo() == 1 - """.format( - module_name=module_name - ), + """, } ) + return str(test_file) @pytest.mark.filterwarnings("default") @@ -39,7 +42,7 @@ def test_normal_flow(testdir, pyfile_with_warnings): """ Check that the warnings section is displayed. """ - result = testdir.runpytest() + result = testdir.runpytest(pyfile_with_warnings) result.stdout.fnmatch_lines( [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, @@ -54,7 +57,7 @@ def test_normal_flow(testdir, pyfile_with_warnings): @pytest.mark.filterwarnings("always") -def test_setup_teardown_warnings(testdir, pyfile_with_warnings): +def test_setup_teardown_warnings(testdir): testdir.makepyfile( """ import warnings @@ -95,7 +98,7 @@ def test_as_errors(testdir, pyfile_with_warnings, method): ) # Use a subprocess, since changing logging level affects other threads # (xdist). - result = testdir.runpytest_subprocess(*args) + result = testdir.runpytest_subprocess(*args, pyfile_with_warnings) result.stdout.fnmatch_lines( [ "E UserWarning: user warning", @@ -116,15 +119,15 @@ def test_ignore(testdir, pyfile_with_warnings, method): """ ) - result = testdir.runpytest(*args) + result = testdir.runpytest(*args, pyfile_with_warnings) result.stdout.fnmatch_lines(["* 1 passed in *"]) assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() @pytest.mark.filterwarnings("always") -def test_unicode(testdir, pyfile_with_warnings): +def test_unicode(testdir): testdir.makepyfile( - """\ + """ import warnings import pytest From aae0579bcde9cf073d2e6b01399b6296299527fa Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 27 Mar 2020 02:40:25 +0100 Subject: [PATCH 049/823] doc: use `envvar` directive for environment variables (#6874) This changes the link anchors in "reference.html", from e.g. `reference.html#pytest-current-test` to `reference.html#envvar-PYTEST_CURRENT_TEST`, but I think that is OK, and not worth adding labels for the old anchors. --- doc/en/example/simple.rst | 8 ++++---- doc/en/flaky.rst | 3 ++- doc/en/reference.rst | 15 +++++---------- src/_pytest/runner.py | 4 ++-- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 984afb152f4..3282bbda584 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -910,9 +910,9 @@ information. Sometimes a test session might get stuck and there might be no easy way to figure out which test got stuck, for example if pytest was run in quiet mode (``-q``) or you don't have access to the console -output. This is particularly a problem if the problem helps only sporadically, the famous "flaky" kind of tests. +output. This is particularly a problem if the problem happens only sporadically, the famous "flaky" kind of tests. -``pytest`` sets a ``PYTEST_CURRENT_TEST`` environment variable when running tests, which can be inspected +``pytest`` sets the :envvar:`PYTEST_CURRENT_TEST` environment variable when running tests, which can be inspected by process monitoring utilities or libraries like `psutil `_ to discover which test got stuck if necessary: @@ -926,8 +926,8 @@ test got stuck if necessary: print(f'pytest process {pid} running: {environ["PYTEST_CURRENT_TEST"]}') During the test session pytest will set ``PYTEST_CURRENT_TEST`` to the current test -:ref:`nodeid ` and the current stage, which can be ``setup``, ``call`` -and ``teardown``. +:ref:`nodeid ` and the current stage, which can be ``setup``, ``call``, +or ``teardown``. For example, when running a single test function named ``test_foo`` from ``foo_module.py``, ``PYTEST_CURRENT_TEST`` will be set to: diff --git a/doc/en/flaky.rst b/doc/en/flaky.rst index 0f0eecab0c8..c246f4d9c18 100644 --- a/doc/en/flaky.rst +++ b/doc/en/flaky.rst @@ -43,7 +43,8 @@ Xfail strict PYTEST_CURRENT_TEST ~~~~~~~~~~~~~~~~~~~ -:ref:`pytest current test env` may be useful for figuring out "which test got stuck". +:envvar:`PYTEST_CURRENT_TEST` may be useful for figuring out "which test got stuck". +See :ref:`pytest current test env` for more details. Plugins diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 02b2f181511..20d2243795e 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -976,19 +976,16 @@ Environment Variables Environment variables that can be used to change pytest's behavior. -PYTEST_ADDOPTS -~~~~~~~~~~~~~~ +.. envvar:: PYTEST_ADDOPTS This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given by the user, see :ref:`adding default options` for more information. -PYTEST_DEBUG -~~~~~~~~~~~~ +.. envvar:: PYTEST_DEBUG When set, pytest will print tracing and debug information. -PYTEST_PLUGINS -~~~~~~~~~~~~~~ +.. envvar:: PYTEST_PLUGINS Contains comma-separated list of modules that should be loaded as plugins: @@ -996,14 +993,12 @@ Contains comma-separated list of modules that should be loaded as plugins: export PYTEST_PLUGINS=mymodule.plugin,xdist -PYTEST_DISABLE_PLUGIN_AUTOLOAD -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. envvar:: PYTEST_DISABLE_PLUGIN_AUTOLOAD When set, disables plugin auto-loading through setuptools entrypoints. Only explicitly specified plugins will be loaded. -PYTEST_CURRENT_TEST -~~~~~~~~~~~~~~~~~~~ +.. envvar:: PYTEST_CURRENT_TEST This is not meant to be set by users, but is set by pytest internally with the name of the current test so other processes can inspect it, see :ref:`pytest current test env` for more information. diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 412ea44a87d..7124996c8ff 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -151,9 +151,9 @@ def pytest_runtest_teardown(item, nextitem): def _update_current_test_var(item, when): """ - Update PYTEST_CURRENT_TEST to reflect the current item and stage. + Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage. - If ``when`` is None, delete PYTEST_CURRENT_TEST from the environment. + If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment. """ var_name = "PYTEST_CURRENT_TEST" if when: From a016a75ca76613335a096aaaade6b4b6d95265e4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Mar 2020 09:54:20 -0300 Subject: [PATCH 050/823] Fix linting --- testing/test_warnings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index fba2c00f9c8..bf7fe51c6f6 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -32,7 +32,7 @@ def foo(): warnings.warn(RuntimeWarning("runtime warning")) return 1 """, - } + }, ) return str(test_file) From a7857545236a8c4c8eb904a43945440849cbbed4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 27 Mar 2020 18:40:23 +0300 Subject: [PATCH 051/823] Change EnvironmentError, IOError to OSError - they are aliases Since Python 3.3, these are aliases for OSError: https://docs.python.org/3/whatsnew/3.3.html#pep-3151-reworking-the-os-and-io-exception-hierarchy --- doc/en/example/assertion/failure_demo.py | 2 +- doc/en/example/reportingdemo.rst | 2 +- doc/en/monkeypatch.rst | 4 ++-- src/_pytest/_code/source.py | 2 +- src/_pytest/assertion/rewrite.py | 12 ++++++------ src/_pytest/cacheprovider.py | 6 +++--- src/_pytest/capture.py | 2 +- src/_pytest/config/findpaths.py | 2 +- src/_pytest/fixtures.py | 2 +- src/_pytest/pathlib.py | 12 ++++++------ testing/acceptance_test.py | 2 +- testing/test_argcomplete.py | 2 +- testing/test_assertrewrite.py | 8 ++++---- testing/test_capture.py | 10 +++++----- testing/test_tmpdir.py | 2 +- 15 files changed, 35 insertions(+), 35 deletions(-) diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index 26454e48d76..4bf827904d8 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -167,7 +167,7 @@ def test_raises(self): raises(TypeError, int, s) def test_raises_doesnt(self): - raises(IOError, int, "3") + raises(OSError, int, "3") def test_raise(self): raise ValueError("demo error") diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 2a1b2ed6562..23c302eca85 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -406,7 +406,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: self = def test_raises_doesnt(self): - > raises(IOError, int, "3") + > raises(OSError, int, "3") E Failed: DID NOT RAISE failure_demo.py:170: Failed diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index 1d1bd68c03a..939fb7ed611 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -268,7 +268,7 @@ to do this using the ``setenv`` and ``delenv`` method. Our example code to test: def get_os_user_lower(): """Simple retrieval function. - Returns lowercase USER or raises EnvironmentError.""" + Returns lowercase USER or raises OSError.""" username = os.getenv("USER") if username is None: @@ -293,7 +293,7 @@ both paths can be safely tested without impacting the running environment: def test_raise_exception(monkeypatch): - """Remove the USER env var and assert EnvironmentError is raised.""" + """Remove the USER env var and assert OSError is raised.""" monkeypatch.delenv("USER", raising=False) with pytest.raises(OSError): diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 28c11e5d5e3..2e44b69d249 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -307,7 +307,7 @@ def getfslineno(obj: Any) -> Tuple[Union[str, py.path.local], int]: if fspath: try: _, lineno = findsource(obj) - except IOError: + except OSError: pass return fspath, lineno else: diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 86aef8a7cc1..ecec2aa3d23 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -284,7 +284,7 @@ def _write_pyc(state, co, source_stat, pyc): try: with atomic_write(fspath(pyc), mode="wb", overwrite=True) as fp: _write_pyc_fp(fp, source_stat, co) - except EnvironmentError as e: + except OSError as e: state.trace("error writing pyc file at {}: {}".format(pyc, e)) # we ignore any failure to write the cache file # there are many reasons, permission-denied, pycache dir being a @@ -299,7 +299,7 @@ def _write_pyc(state, co, source_stat, pyc): proc_pyc = "{}.{}".format(pyc, os.getpid()) try: fp = open(proc_pyc, "wb") - except EnvironmentError as e: + except OSError as e: state.trace( "error writing pyc file at {}: errno={}".format(proc_pyc, e.errno) ) @@ -308,7 +308,7 @@ def _write_pyc(state, co, source_stat, pyc): try: _write_pyc_fp(fp, source_stat, co) os.rename(proc_pyc, fspath(pyc)) - except EnvironmentError as e: + except OSError as e: state.trace("error writing pyc file at {}: {}".format(pyc, e)) # we ignore any failure to write the cache file # there are many reasons, permission-denied, pycache dir being a @@ -338,7 +338,7 @@ def _read_pyc(source, pyc, trace=lambda x: None): """ try: fp = open(fspath(pyc), "rb") - except IOError: + except OSError: return None with fp: try: @@ -346,8 +346,8 @@ def _read_pyc(source, pyc, trace=lambda x: None): mtime = int(stat_result.st_mtime) size = stat_result.st_size data = fp.read(12) - except EnvironmentError as e: - trace("_read_pyc({}): EnvironmentError {}".format(source, e)) + except OSError as e: + trace("_read_pyc({}): OSError {}".format(source, e)) return None # Check for invalid or out of date pyc file. if ( diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index a0f486089ff..2f13067bc00 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -121,7 +121,7 @@ def get(self, key, default): try: with path.open("r") as f: return json.load(f) - except (ValueError, IOError, OSError): + except (ValueError, OSError): return default def set(self, key, value): @@ -140,7 +140,7 @@ def set(self, key, value): else: cache_dir_exists_already = self._cachedir.exists() path.parent.mkdir(exist_ok=True, parents=True) - except (IOError, OSError): + except OSError: self.warn("could not create cache path {path}", path=path) return if not cache_dir_exists_already: @@ -148,7 +148,7 @@ def set(self, key, value): data = json.dumps(value, indent=2, sort_keys=True) try: f = path.open("w") - except (IOError, OSError): + except OSError: self.warn("cache could not write path {path}", path=path) else: with f: diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 90de1d9fce7..7096f95b298 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -689,7 +689,7 @@ class DontReadFromInput: encoding = None def read(self, *args): - raise IOError( + raise OSError( "pytest: reading from stdin while output is captured! Consider using `-s`." ) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index fb84160c1ff..dae778c9374 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -15,7 +15,7 @@ from . import Config # noqa: F401 -def exists(path, ignore=EnvironmentError): +def exists(path, ignore=OSError): try: return path.check() except ignore: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 081e95a6db8..4c3d0d4bbb5 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -715,7 +715,7 @@ def formatrepr(self) -> "FixtureLookupErrorRepr": fspath, lineno = getfslineno(function) try: lines, _ = inspect.getsourcelines(get_real_func(function)) - except (IOError, IndexError, TypeError): + except (OSError, IndexError, TypeError): error_msg = "file %s, line %s: source code not available" addline(error_msg % (fspath, lineno + 1)) else: diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 8d25b21dd7d..21ec61e2c5b 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -178,7 +178,7 @@ def make_numbered_dir(root: Path, prefix: str) -> Path: _force_symlink(root, prefix + "current", new_path) return new_path else: - raise EnvironmentError( + raise OSError( "could not create numbered dir with prefix " "{prefix} in {root} after 10 tries".format(prefix=prefix, root=root) ) @@ -190,14 +190,14 @@ def create_cleanup_lock(p: Path) -> Path: try: fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) except FileExistsError as e: - raise EnvironmentError("cannot create lockfile in {path}".format(path=p)) from e + raise OSError("cannot create lockfile in {path}".format(path=p)) from e else: pid = os.getpid() spid = str(pid).encode() os.write(fd, spid) os.close(fd) if not lock_path.is_file(): - raise EnvironmentError("lock path got renamed after successful creation") + raise OSError("lock path got renamed after successful creation") return lock_path @@ -212,7 +212,7 @@ def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> Non return try: lock_path.unlink() - except (OSError, IOError): + except OSError: pass return register(cleanup_on_exit) @@ -228,7 +228,7 @@ def maybe_delete_a_numbered_dir(path: Path) -> None: garbage = parent.joinpath("garbage-{}".format(uuid.uuid4())) path.rename(garbage) rm_rf(garbage) - except (OSError, EnvironmentError): + except OSError: # known races: # * other process did a cleanup at the same time # * deletable folder was found @@ -240,7 +240,7 @@ def maybe_delete_a_numbered_dir(path: Path) -> None: if lock_path is not None: try: lock_path.unlink() - except (OSError, IOError): + except OSError: pass diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 861938617e8..1bfdeed380e 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -463,7 +463,7 @@ def test_getsourcelines_error_issue553(self, testdir, monkeypatch): p = testdir.makepyfile( """ def raise_error(obj): - raise IOError('source code not available') + raise OSError('source code not available') import inspect inspect.getsourcelines = raise_error diff --git a/testing/test_argcomplete.py b/testing/test_argcomplete.py index 7ccca11ba70..08362c62a63 100644 --- a/testing/test_argcomplete.py +++ b/testing/test_argcomplete.py @@ -20,7 +20,7 @@ def equal_with_bash(prefix, ffc, fc, out=None): # copied from argcomplete.completers as import from there # also pulls in argcomplete.__init__ which opens filedescriptor 9 -# this gives an IOError at the end of testrun +# this gives an OSError at the end of testrun def _wrapcall(*args, **kargs): diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 91a3a35e2e2..04e0c6f9e94 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -971,7 +971,7 @@ def test_write_pyc(self, testdir, tmpdir, monkeypatch): @contextmanager def atomic_write_failed(fn, mode="r", overwrite=False): - e = IOError() + e = OSError() e.errno = 10 raise e yield @@ -981,10 +981,10 @@ def atomic_write_failed(fn, mode="r", overwrite=False): ) else: - def raise_ioerror(*args): - raise IOError() + def raise_oserror(*args): + raise OSError() - monkeypatch.setattr("os.rename", raise_ioerror) + monkeypatch.setattr("os.rename", raise_oserror) assert not _write_pyc(state, [1], os.stat(source_path), pycpath) diff --git a/testing/test_capture.py b/testing/test_capture.py index 8b6e5967576..49269ee96de 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -829,10 +829,10 @@ def test_dontreadfrominput(): f = DontReadFromInput() assert f.buffer is f assert not f.isatty() - pytest.raises(IOError, f.read) - pytest.raises(IOError, f.readlines) + pytest.raises(OSError, f.read) + pytest.raises(OSError, f.readlines) iter_f = iter(f) - pytest.raises(IOError, next, iter_f) + pytest.raises(OSError, next, iter_f) pytest.raises(UnsupportedOperation, f.fileno) f.close() # just for completeness @@ -1083,7 +1083,7 @@ def test_stdin_nulled_by_default(self): print("XXX which indicates an error in the underlying capturing") print("XXX mechanisms") with self.getcapture(): - pytest.raises(IOError, sys.stdin.read) + pytest.raises(OSError, sys.stdin.read) class TestTeeStdCapture(TestStdCapture): @@ -1356,7 +1356,7 @@ def test_spam_in_thread(): result = testdir.runpytest_subprocess(str(p)) assert result.ret == 0 assert result.stderr.str() == "" - result.stdout.no_fnmatch_line("*IOError*") + result.stdout.no_fnmatch_line("*OSError*") def test_global_capture_with_live_logging(testdir): diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index b7cf8d2b5c6..1c3b32ae490 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -264,7 +264,7 @@ def test_cleanup_lock_create(self, tmp_path): from _pytest.pathlib import create_cleanup_lock lockfile = create_cleanup_lock(d) - with pytest.raises(EnvironmentError, match="cannot create lockfile in .*"): + with pytest.raises(OSError, match="cannot create lockfile in .*"): create_cleanup_lock(d) lockfile.unlink() From 0e4a44db3b3f29c06949e3093b90d5029ebc195c Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 27 Mar 2020 20:46:42 +0100 Subject: [PATCH 052/823] Better document xfail(condition) (#6957) --- doc/en/reference.rst | 3 ++- doc/en/skipping.rst | 54 +++++++++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 20d2243795e..1c94ca07d9d 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -204,7 +204,8 @@ Marks a test function as *expected to fail*. :type condition: bool or str :param condition: Condition for marking the test function as xfail (``True/False`` or a - :ref:`condition string `). + :ref:`condition string `). If a bool, you also have + to specify ``reason`` (see :ref:`condition string `). :keyword str reason: Reason why the test function is marked as xfail. :keyword Exception raises: Exception subclass expected to be raised by the test function; other exceptions will fail the test. :keyword bool run: diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 73ce4868976..db3c90ad5ea 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -265,33 +265,20 @@ internally by raising a known exception. **Reference**: :ref:`pytest.mark.xfail ref` -.. _`xfail strict tutorial`: - -``strict`` parameter -~~~~~~~~~~~~~~~~~~~~ - - +``condition`` parameter +~~~~~~~~~~~~~~~~~~~~~~~ -Both ``XFAIL`` and ``XPASS`` don't fail the test suite by default. -You can change this by setting the ``strict`` keyword-only parameter to ``True``: +If a test is only expected to fail under a certain condition, you can pass +that condition as the first parameter: .. code-block:: python - @pytest.mark.xfail(strict=True) + @pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library") def test_function(): ... - -This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite. - -You can change the default value of the ``strict`` parameter using the -``xfail_strict`` ini option: - -.. code-block:: ini - - [pytest] - xfail_strict=true - +Note that you have to pass a reason as well (see the parameter description at +:ref:`pytest.mark.xfail ref`). ``reason`` parameter ~~~~~~~~~~~~~~~~~~~~ @@ -301,7 +288,7 @@ on a particular platform: .. code-block:: python - @pytest.mark.xfail(sys.version_info >= (3, 6), reason="python3.6 api changes") + @pytest.mark.xfail(reason="known parser issue") def test_function(): ... @@ -336,6 +323,31 @@ even executed, use the ``run`` parameter as ``False``: This is specially useful for xfailing tests that are crashing the interpreter and should be investigated later. +.. _`xfail strict tutorial`: + +``strict`` parameter +~~~~~~~~~~~~~~~~~~~~ + +Both ``XFAIL`` and ``XPASS`` don't fail the test suite by default. +You can change this by setting the ``strict`` keyword-only parameter to ``True``: + +.. code-block:: python + + @pytest.mark.xfail(strict=True) + def test_function(): + ... + + +This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite. + +You can change the default value of the ``strict`` parameter using the +``xfail_strict`` ini option: + +.. code-block:: ini + + [pytest] + xfail_strict=true + Ignoring xfail ~~~~~~~~~~~~~~ From 70cbce7ccc46eaee4b95e77cbbcf5c9f506b6308 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 28 Mar 2020 11:28:54 -0300 Subject: [PATCH 053/823] Quick doc fix on xfail reason parameter As per https://github.com/pytest-dev/pytest/pull/6957/files#r399564043 --- doc/en/skipping.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index db3c90ad5ea..1a85ecf070a 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -283,8 +283,7 @@ Note that you have to pass a reason as well (see the parameter description at ``reason`` parameter ~~~~~~~~~~~~~~~~~~~~ -As with skipif_ you can also mark your expectation of a failure -on a particular platform: +You can specify the motive of an expected failure with the ``reason`` parameter: .. code-block:: python From 95fadd5740d05fda9c525e1e7d8e75b60ad1b143 Mon Sep 17 00:00:00 2001 From: smarie Date: Sun, 29 Mar 2020 14:20:09 +0200 Subject: [PATCH 054/823] Improved time counter used to compute test durations. (#6939) Co-authored-by: Sylvain MARIE Co-authored-by: Ran Benita Co-authored-by: Bruno Oliveira --- AUTHORS | 1 + changelog/4391.improvement.rst | 1 + changelog/6940.improvement.rst | 1 + src/_pytest/reports.py | 2 +- src/_pytest/runner.py | 45 ++++++++++++++++++++++++++-------- testing/acceptance_test.py | 44 ++++++++++++++++++++++++--------- 6 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 changelog/4391.improvement.rst create mode 100644 changelog/6940.improvement.rst diff --git a/AUTHORS b/AUTHORS index af0dc62c4d8..7c791cde8ca 100644 --- a/AUTHORS +++ b/AUTHORS @@ -254,6 +254,7 @@ Stefano Taschini Steffen Allner Stephan Obermann Sven-Hendrik Haase +Sylvain Marié Tadek Teleżyński Takafumi Arakaki Tarcisio Fischer diff --git a/changelog/4391.improvement.rst b/changelog/4391.improvement.rst new file mode 100644 index 00000000000..e7e4090f1dd --- /dev/null +++ b/changelog/4391.improvement.rst @@ -0,0 +1 @@ +Improved precision of test durations measurement. ``CallInfo`` items now have a new ``.duration`` attribute, created using ``time.perf_counter()``. This attribute is used to fill the ``.duration`` attribute, which is more accurate than the previous ``.stop - .start`` (as these are based on ``time.time()``). diff --git a/changelog/6940.improvement.rst b/changelog/6940.improvement.rst new file mode 100644 index 00000000000..ab5fc0d49bf --- /dev/null +++ b/changelog/6940.improvement.rst @@ -0,0 +1 @@ +When using the ``--duration`` option, the terminal message output is now more precise about the number and durations of hidden items. diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index dfda7f0e382..8459c1cb9e4 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -273,7 +273,7 @@ def from_item_and_call(cls, item, call) -> "TestReport": Factory method to create and fill a TestReport with standard item and call info. """ when = call.when - duration = call.stop - call.start + duration = call.duration keywords = {x: 1 for x in item.keywords} excinfo = call.excinfo sections = [] diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 7124996c8ff..c13dff711a1 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -2,6 +2,7 @@ import bdb import os import sys +from time import perf_counter from time import time from typing import Callable from typing import Dict @@ -59,15 +60,18 @@ def pytest_terminal_summary(terminalreporter): dlist.sort(key=lambda x: x.duration) dlist.reverse() if not durations: - tr.write_sep("=", "slowest test durations") + tr.write_sep("=", "slowest durations") else: - tr.write_sep("=", "slowest %s test durations" % durations) + tr.write_sep("=", "slowest %s durations" % durations) dlist = dlist[:durations] - for rep in dlist: + for i, rep in enumerate(dlist): if verbose < 2 and rep.duration < 0.005: tr.write_line("") - tr.write_line("(0.00 durations hidden. Use -vv to show these durations.)") + tr.write_line( + "(%s durations < 0.005s hidden. Use -vv to show these durations.)" + % (len(dlist) - i) + ) break tr.write_line("{:02.2f}s {:<8} {}".format(rep.duration, rep.when, rep.nodeid)) @@ -220,13 +224,23 @@ def call_runtest_hook(item, when: "Literal['setup', 'call', 'teardown']", **kwds @attr.s(repr=False) class CallInfo: - """ Result/Exception info a function invocation. """ + """ Result/Exception info a function invocation. + + :param result: The return value of the call, if it didn't raise. Can only be accessed + if excinfo is None. + :param Optional[ExceptionInfo] excinfo: The captured exception of the call, if it raised. + :param float start: The system time when the call started, in seconds since the epoch. + :param float stop: The system time when the call ended, in seconds since the epoch. + :param float duration: The call duration, in seconds. + :param str when: The context of invocation: "setup", "call", "teardown", ... + """ _result = attr.ib() excinfo = attr.ib(type=Optional[ExceptionInfo]) - start = attr.ib() - stop = attr.ib() - when = attr.ib() + start = attr.ib(type=float) + stop = attr.ib(type=float) + duration = attr.ib(type=float) + when = attr.ib(type=str) @property def result(self): @@ -238,8 +252,9 @@ def result(self): def from_call(cls, func, when, reraise=None) -> "CallInfo": #: context of invocation: one of "setup", "call", #: "teardown", "memocollect" - start = time() excinfo = None + start = time() + precise_start = perf_counter() try: result = func() except: # noqa @@ -247,8 +262,18 @@ def from_call(cls, func, when, reraise=None) -> "CallInfo": if reraise is not None and excinfo.errisinstance(reraise): raise result = None + # use the perf counter + precise_stop = perf_counter() + duration = precise_stop - precise_start stop = time() - return cls(start=start, stop=stop, when=when, result=result, excinfo=excinfo) + return cls( + start=start, + stop=stop, + duration=duration, + when=when, + result=result, + excinfo=excinfo, + ) def __repr__(self): if self.excinfo is None: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 1bfdeed380e..c28c35b8621 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -896,26 +896,42 @@ def test_has_plugin(self, request): class TestDurations: source = """ import time - frag = 0.002 + frag = 0.002 # 2 ms def test_something(): pass def test_2(): - time.sleep(frag*5) + time.sleep(frag*5) # 10 ms: on windows might sleep < 0.005s def test_1(): - time.sleep(frag) + time.sleep(frag) # 2 ms: on macOS/windows might sleep > 0.005s def test_3(): - time.sleep(frag*10) + time.sleep(frag*10) # 20 ms """ def test_calls(self, testdir): testdir.makepyfile(self.source) result = testdir.runpytest("--durations=10") assert result.ret == 0 - result.stdout.fnmatch_lines_random( - ["*durations*", "*call*test_3*", "*call*test_2*"] - ) + + # on Windows, test 2 (10ms) can actually sleep less than 5ms and become hidden + if sys.platform == "win32": + to_match = ["*durations*", "*call*test_3*"] + else: + to_match = ["*durations*", "*call*test_3*", "*call*test_2*"] + result.stdout.fnmatch_lines_random(to_match) + + # The number of hidden should be 8, but on macOS and windows it sometimes is 7 + # - on MacOS and Windows test 1 can last longer and appear in the list + # - on Windows test 2 can last less and disappear from the list + if sys.platform in ("win32", "darwin"): + nb_hidden = "*" + else: + nb_hidden = "8" + result.stdout.fnmatch_lines( - ["(0.00 durations hidden. Use -vv to show these durations.)"] + [ + "(%s durations < 0.005s hidden. Use -vv to show these durations.)" + % nb_hidden + ] ) def test_calls_show_2(self, testdir): @@ -929,7 +945,10 @@ def test_calls_showall(self, testdir): testdir.makepyfile(self.source) result = testdir.runpytest("--durations=0") assert result.ret == 0 - for x in "23": + + # on windows, test 2 (10ms) can actually sleep less than 5ms and become hidden + tested = "3" if sys.platform == "win32" else "23" + for x in tested: for y in ("call",): # 'setup', 'call', 'teardown': for line in result.stdout.lines: if ("test_%s" % x) in line and y in line: @@ -951,9 +970,10 @@ def test_calls_showall_verbose(self, testdir): def test_with_deselected(self, testdir): testdir.makepyfile(self.source) - result = testdir.runpytest("--durations=2", "-k test_2") + # on windows test 2 might sleep less than 0.005s and be hidden. Prefer test 3. + result = testdir.runpytest("--durations=2", "-k test_3") assert result.ret == 0 - result.stdout.fnmatch_lines(["*durations*", "*call*test_2*"]) + result.stdout.fnmatch_lines(["*durations*", "*call*test_3*"]) def test_with_failing_collection(self, testdir): testdir.makepyfile(self.source) @@ -975,7 +995,7 @@ class TestDurationWithFixture: source = """ import pytest import time - frag = 0.01 + frag = 0.02 # as on windows sleep(0.01) might take < 0.005s @pytest.fixture def setup_fixt(): From 285beddf2839f149cd33b4092718234c978fc687 Mon Sep 17 00:00:00 2001 From: Nikolay Kondratyev <4085884+kondratyev-nv@users.noreply.github.com> Date: Sun, 29 Mar 2020 16:55:56 +0300 Subject: [PATCH 055/823] Fix documentation typo --- doc/en/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 1c94ca07d9d..0059b4cb278 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1028,7 +1028,7 @@ file, usually located at the root of your repository. All options must be under down problems. When possible, it is recommended to use the latter files to hold your pytest configuration. -Configuration file options may be overwritten in the command-line by using ``-o/--override``, which can also be +Configuration file options may be overwritten in the command-line by using ``-o/--override-ini``, which can also be passed multiple times. The expected format is ``name=value``. For example:: pytest -o console_output_style=classic -o cache_dir=/tmp/mycache From eab2831671a4c5e521f573119e06b00b52535278 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 30 Mar 2020 21:31:53 +0200 Subject: [PATCH 056/823] fix #6951: allow to write TerminalReporter.writer --- changelog/6951.bugfix.rst | 1 + src/_pytest/deprecated.py | 6 ++++++ src/_pytest/terminal.py | 13 +++++++------ testing/deprecated_test.py | 9 ++++++++- 4 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 changelog/6951.bugfix.rst diff --git a/changelog/6951.bugfix.rst b/changelog/6951.bugfix.rst new file mode 100644 index 00000000000..984089b3a56 --- /dev/null +++ b/changelog/6951.bugfix.rst @@ -0,0 +1 @@ +Allow users to still set the deprecated ``TerminalReporter.writer`` attribute. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 28ca02550d4..ee27b20ec65 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -55,3 +55,9 @@ "The pytest_collect_directory hook is not working.\n" "Please use collect_ignore in conftests or pytest_collection_modifyitems." ) + + +TERMINALWRITER_WRITER = PytestDeprecationWarning( + "The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.\n" + "See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information." +) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index b0a2d253056..a99463fe875 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -29,6 +29,7 @@ from _pytest._io import TerminalWriter from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.deprecated import TERMINALWRITER_WRITER from _pytest.main import Session from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -284,14 +285,14 @@ def __init__(self, config: Config, file=None) -> None: @property def writer(self) -> TerminalWriter: - warnings.warn( - pytest.PytestDeprecationWarning( - "TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.\n" - "See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information." - ) - ) + warnings.warn(TERMINALWRITER_WRITER, stacklevel=2) return self._tw + @writer.setter + def writer(self, value: TerminalWriter): + warnings.warn(TERMINALWRITER_WRITER, stacklevel=2) + self._tw = value + def _determine_show_progress_info(self): """Return True if we should display progress information based on the current config""" # do not show progress if we are not capturing output (#3038) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index b5c66d9f5f1..ce54783f403 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -36,8 +36,15 @@ def test_terminal_reporter_writer_attr(pytestconfig): except ImportError: pass terminal_reporter = pytestconfig.pluginmanager.get_plugin("terminalreporter") + expected_tw = terminal_reporter._tw + + with pytest.warns(pytest.PytestDeprecationWarning): + assert terminal_reporter.writer is expected_tw + with pytest.warns(pytest.PytestDeprecationWarning): - assert terminal_reporter.writer is terminal_reporter._tw + terminal_reporter.writer = expected_tw + + assert terminal_reporter._tw is expected_tw @pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS)) From f1d51ba1f57619e71cdf787ff1507be59a057c73 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 29 Mar 2020 20:30:16 +0200 Subject: [PATCH 057/823] deprecate the pytest.collect module changelog minimal unittest for collect module deprecations \!fixup - changelog typo --- changelog/6981.deprecation.rst | 1 + src/_pytest/compat.py | 24 --------------------- src/_pytest/deprecated.py | 6 ++++++ src/pytest/__init__.py | 8 ++----- src/pytest/collect.py | 38 ++++++++++++++++++++++++++++++++++ testing/deprecated_test.py | 7 +++++++ 6 files changed, 54 insertions(+), 30 deletions(-) create mode 100644 changelog/6981.deprecation.rst create mode 100644 src/pytest/collect.py diff --git a/changelog/6981.deprecation.rst b/changelog/6981.deprecation.rst new file mode 100644 index 00000000000..ac32706faec --- /dev/null +++ b/changelog/6981.deprecation.rst @@ -0,0 +1 @@ +Deprecate the ``pytest.collect`` module as it's just aliases into ``pytest``. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 1845d9d91ef..8aff8d57da4 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -336,30 +336,6 @@ def safe_isclass(obj: object) -> bool: return False -COLLECT_FAKEMODULE_ATTRIBUTES = ( - "Collector", - "Module", - "Function", - "Instance", - "Session", - "Item", - "Class", - "File", - "_fillfuncargs", -) - - -def _setup_collect_fakemodule() -> None: - from types import ModuleType - import pytest - - # Types ignored because the module is created dynamically. - pytest.collect = ModuleType("pytest.collect") # type: ignore - pytest.collect.__all__ = [] # type: ignore # used for setns - for attr_name in COLLECT_FAKEMODULE_ATTRIBUTES: - setattr(pytest.collect, attr_name, getattr(pytest, attr_name)) # type: ignore - - class CaptureIO(io.TextIOWrapper): def __init__(self) -> None: super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index ee27b20ec65..d8486461899 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -56,6 +56,12 @@ "Please use collect_ignore in conftests or pytest_collection_modifyitems." ) +PYTEST_COLLECT_MODULE = UnformattedWarning( + PytestDeprecationWarning, + "pytest.collect.{name} was moved to pytest.{name}\n" + "Please update to the new name.", +) + TERMINALWRITER_WRITER = PytestDeprecationWarning( "The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.\n" diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 33bc3d0fbe5..5c93decc3c5 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -2,9 +2,9 @@ """ pytest: unit and functional testing with Python. """ +from . import collect from _pytest import __version__ from _pytest.assertion import register_assert_rewrite -from _pytest.compat import _setup_collect_fakemodule from _pytest.config import cmdline from _pytest.config import ExitCode from _pytest.config import hookimpl @@ -46,7 +46,6 @@ from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestWarning - set_trace = __pytestPDB.set_trace __all__ = [ @@ -55,6 +54,7 @@ "approx", "Class", "cmdline", + "collect", "Collector", "deprecated_call", "exit", @@ -93,7 +93,3 @@ "xfail", "yield_fixture", ] - - -_setup_collect_fakemodule() -del _setup_collect_fakemodule diff --git a/src/pytest/collect.py b/src/pytest/collect.py new file mode 100644 index 00000000000..73c9d35a0df --- /dev/null +++ b/src/pytest/collect.py @@ -0,0 +1,38 @@ +import sys +import warnings +from types import ModuleType + +import pytest +from _pytest.deprecated import PYTEST_COLLECT_MODULE + + +COLLECT_FAKEMODULE_ATTRIBUTES = [ + "Collector", + "Module", + "Function", + "Instance", + "Session", + "Item", + "Class", + "File", + "_fillfuncargs", +] + + +class FakeCollectModule(ModuleType): + def __init__(self): + super().__init__("pytest.collect") + self.__all__ = list(COLLECT_FAKEMODULE_ATTRIBUTES) + self.__pytest = pytest + + def __dir__(self): + return dir(super()) + self.__all__ + + def __getattr__(self, name): + if name not in self.__all__: + raise AttributeError(name) + warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2) + return getattr(pytest, name) + + +sys.modules["pytest.collect"] = FakeCollectModule() diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index ce54783f403..93601d0a930 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -25,6 +25,13 @@ def test(): ) +@pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore +# false positive due to dynamic attribute +def test_pytest_collect_module_deprecated(attribute): + with pytest.warns(DeprecationWarning, match=attribute): + getattr(pytest.collect, attribute) + + def test_terminal_reporter_writer_attr(pytestconfig): """Check that TerminalReporter._tw is also available as 'writer' (#2984) This attribute has been deprecated in 5.4. From 451aef65ac5a3b78441f188995978f9ad5eea812 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 14 Mar 2020 18:50:43 +0100 Subject: [PATCH 058/823] prepare tests and disable warnings for asyncio unittest cases shoehorn unittest async results into python test result interpretation changelog --- changelog/6924.bugfix.rst | 1 + src/_pytest/python.py | 26 ++++++++++++++++--- testing/example_scripts/pytest.ini | 2 ++ .../unittest/test_unittest_asyncio.py | 15 +++++++++++ testing/test_unittest.py | 8 ++++++ 5 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 changelog/6924.bugfix.rst create mode 100644 testing/example_scripts/pytest.ini create mode 100644 testing/example_scripts/unittest/test_unittest_asyncio.py diff --git a/changelog/6924.bugfix.rst b/changelog/6924.bugfix.rst new file mode 100644 index 00000000000..7283370a0e1 --- /dev/null +++ b/changelog/6924.bugfix.rst @@ -0,0 +1 @@ +Ensure a ``unittest.IsolatedAsyncioTestCase`` is actually awaited. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e260761794d..1f6a095c4e1 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -175,15 +175,33 @@ def async_warn(nodeid: str) -> None: @hookimpl(trylast=True) def pytest_pyfunc_call(pyfuncitem: "Function"): testfunction = pyfuncitem.obj - if iscoroutinefunction(testfunction) or ( - sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction) - ): + + try: + # ignoring type as the import is invalid in py37 and mypy thinks its a error + from unittest import IsolatedAsyncioTestCase # type: ignore + except ImportError: + async_ok_in_stdlib = False + else: + async_ok_in_stdlib = isinstance( + getattr(testfunction, "__self__", None), IsolatedAsyncioTestCase + ) + + if ( + iscoroutinefunction(testfunction) + or (sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction)) + ) and not async_ok_in_stdlib: async_warn(pyfuncitem.nodeid) funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} result = testfunction(**testargs) if hasattr(result, "__await__") or hasattr(result, "__aiter__"): - async_warn(pyfuncitem.nodeid) + if async_ok_in_stdlib: + # todo: investigate moving this to the unittest plugin + # by a test call result hook + testcase = testfunction.__self__ + testcase._callMaybeAsync(lambda: result) + else: + async_warn(pyfuncitem.nodeid) return True diff --git a/testing/example_scripts/pytest.ini b/testing/example_scripts/pytest.ini new file mode 100644 index 00000000000..ec5fe0e83a7 --- /dev/null +++ b/testing/example_scripts/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +# dummy pytest.ini to ease direct running of example scripts diff --git a/testing/example_scripts/unittest/test_unittest_asyncio.py b/testing/example_scripts/unittest/test_unittest_asyncio.py new file mode 100644 index 00000000000..16eec1026ff --- /dev/null +++ b/testing/example_scripts/unittest/test_unittest_asyncio.py @@ -0,0 +1,15 @@ +from unittest import IsolatedAsyncioTestCase # type: ignore + + +class AsyncArguments(IsolatedAsyncioTestCase): + async def test_something_async(self): + async def addition(x, y): + return x + y + + self.assertEqual(await addition(2, 2), 4) + + async def test_something_async_fails(self): + async def addition(x, y): + return x + y + + self.assertEqual(await addition(2, 2), 3) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index c5fc20239b1..de51f7bd104 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1129,3 +1129,11 @@ def test(self): result = testdir.runpytest("--trace", str(p1)) assert len(calls) == 2 assert result.ret == 0 + + +def test_async_support(testdir): + pytest.importorskip("unittest.async_case") + + testdir.copy_example("unittest/test_unittest_asyncio.py") + reprec = testdir.inline_run() + reprec.assertoutcome(failed=1, passed=1) From ff0a091165c4a9a907b0ebe1dd31953b5682423f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 30 Mar 2020 23:40:33 +0200 Subject: [PATCH 059/823] Fix/improve test_terminal_reporter_writer_attr It did not actually test that the attribute gets set. This also checks the stacklevel etc. --- testing/deprecated_test.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 93601d0a930..fc89c775189 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,8 +1,10 @@ +import copy import inspect import pytest from _pytest import deprecated from _pytest import nodes +from _pytest.config import Config @pytest.mark.filterwarnings("default") @@ -32,7 +34,7 @@ def test_pytest_collect_module_deprecated(attribute): getattr(pytest.collect, attribute) -def test_terminal_reporter_writer_attr(pytestconfig): +def test_terminal_reporter_writer_attr(pytestconfig: Config) -> None: """Check that TerminalReporter._tw is also available as 'writer' (#2984) This attribute has been deprecated in 5.4. """ @@ -43,15 +45,22 @@ def test_terminal_reporter_writer_attr(pytestconfig): except ImportError: pass terminal_reporter = pytestconfig.pluginmanager.get_plugin("terminalreporter") - expected_tw = terminal_reporter._tw - - with pytest.warns(pytest.PytestDeprecationWarning): - assert terminal_reporter.writer is expected_tw - - with pytest.warns(pytest.PytestDeprecationWarning): - terminal_reporter.writer = expected_tw - - assert terminal_reporter._tw is expected_tw + original_tw = terminal_reporter._tw + + with pytest.warns(pytest.PytestDeprecationWarning) as cw: + assert terminal_reporter.writer is original_tw + assert len(cw) == 1 + assert cw[0].filename == __file__ + + new_tw = copy.copy(original_tw) + with pytest.warns(pytest.PytestDeprecationWarning) as cw: + terminal_reporter.writer = new_tw + try: + assert terminal_reporter._tw is new_tw + finally: + terminal_reporter.writer = original_tw + assert len(cw) == 2 + assert cw[0].filename == cw[1].filename == __file__ @pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS)) From 607f7603afdbaf2dcace7f1ed33af34a1d245291 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 31 Mar 2020 09:38:51 +0200 Subject: [PATCH 060/823] doc: pytest_collection: has to set `session.items` Would make sense to use its return value etc, but this helps for now. --- src/_pytest/hookspec.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 1e16d092d0b..6db543febd8 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -170,6 +170,8 @@ def pytest_load_initial_conftests(early_config, parser, args): def pytest_collection(session: "Session") -> Optional[Any]: """Perform the collection protocol for the given session. + The hook has to set `session.items` to a sequence of items. + Stops at first non-None result, see :ref:`firstresult`. :param _pytest.main.Session session: the pytest session object From 7d75762de6f7b99fcbf9c4ea5f1fa55090b187f6 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Wed, 1 Apr 2020 09:43:54 -0400 Subject: [PATCH 061/823] Do not use automatic title in fixture reference It creates odd wording otherwise. Keep the reference, update the title using Sphinx notation. --- doc/en/cache.rst | 2 +- doc/en/example/index.rst | 2 +- doc/en/faq.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/cache.rst b/doc/en/cache.rst index b01182d9885..fa51bd5ee9f 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -194,7 +194,7 @@ The new config.cache object Plugins or conftest.py support code can get a cached value using the pytest ``config`` object. Here is a basic example plugin which -implements a :ref:`fixture` which re-uses previously created state +implements a :ref:`fixture ` which re-uses previously created state across pytest invocations: .. code-block:: python diff --git a/doc/en/example/index.rst b/doc/en/example/index.rst index f63cb822a41..6876082d418 100644 --- a/doc/en/example/index.rst +++ b/doc/en/example/index.rst @@ -15,7 +15,7 @@ For basic examples, see - :doc:`../getting-started` for basic introductory examples - :ref:`assert` for basic assertion examples -- :ref:`fixtures` for basic fixture/setup examples +- :ref:`Fixtures ` for basic fixture/setup examples - :ref:`parametrize` for basic test function parametrization - :doc:`../unittest` for basic unittest integration - :doc:`../nose` for basic nosetests integration diff --git a/doc/en/faq.rst b/doc/en/faq.rst index 42a2a847bcd..c281debe8cc 100644 --- a/doc/en/faq.rst +++ b/doc/en/faq.rst @@ -32,7 +32,7 @@ just run your tests even if you return Deferreds. In addition, there also is a dedicated `pytest-twisted `_ plugin which allows you to return deferreds from pytest-style tests, allowing the use of -:ref:`fixtures` and other features. +:ref:`fixtures ` and other features. how does pytest work with Django? ++++++++++++++++++++++++++++++++++++++++++++++ From 354602abe6c06ec874cd091c56f0c548b17f796d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 2 Apr 2020 12:01:43 +0200 Subject: [PATCH 062/823] Update src/_pytest/hookspec.py Co-Authored-By: Bruno Oliveira --- src/_pytest/hookspec.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 6db543febd8..5edec31d535 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -170,7 +170,26 @@ def pytest_load_initial_conftests(early_config, parser, args): def pytest_collection(session: "Session") -> Optional[Any]: """Perform the collection protocol for the given session. - The hook has to set `session.items` to a sequence of items. + Usually plugins will implement this hook only to perform some action before + collection, for example the terminal plugin will use this to start displaying + the collection counter, so usually plugins return `None` from this hook after + performing the desired operation. + + However a plugin might decide to override the collection completely, + in which case it should return `True`, but then it is usually expected + that this hook will also need to perform the following operations + that are usually part of the collection process: + + 1. Call the pytest_collectstart hook. + 2. Call the pytest_collectreport hook. + 3. Call the pytest_collection_modifyitems hook. + 4. Call the pytest_collection_finish hook. + 5. Set session.testscollected to the amount of collect items. + 6. Set `session.items` to a list of items. + + If a plugin just wants to skip collection entirely, like `pytest-xdist` + does for master nodes, then it is OK to not do anything other than + returning `True` from here. Stops at first non-None result, see :ref:`firstresult`. From 03451c397f166b3b1999e49f190cd6880b5cf0a6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Apr 2020 18:22:15 +0300 Subject: [PATCH 063/823] Simplify positional arguments compatibility code in pytest.fixture() The dynamic scope feature added in 10bf6aac76d5060a0db4a94871d6dcf0a1 necessitated some wrangling of arguments in pytest.fixture(). In particular, it deprecated positional arguments in favor of keyword-only arguments, while keeping backward compatibility. The way it did this avoided some code duplication but ended up being quite hard to follow and to annotate with types. Replace it with some straightforward code, which is not very DRY but is simple and easy to remove when the time comes. --- src/_pytest/fixtures.py | 110 +++++++++++++++++-------------------- testing/python/fixtures.py | 39 ++++++++++++- 2 files changed, 87 insertions(+), 62 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 4c3d0d4bbb5..f547a7eab6f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1031,50 +1031,8 @@ def __call__(self, function): return function -FIXTURE_ARGS_ORDER = ("scope", "params", "autouse", "ids", "name") - - -def _parse_fixture_args(callable_or_scope, *args, **kwargs): - arguments = { - "scope": "function", - "params": None, - "autouse": False, - "ids": None, - "name": None, - } - kwargs = { - key: value for key, value in kwargs.items() if arguments.get(key) != value - } - - fixture_function = None - if isinstance(callable_or_scope, str): - args = list(args) - args.insert(0, callable_or_scope) - else: - fixture_function = callable_or_scope - - positionals = set() - for positional, argument_name in zip(args, FIXTURE_ARGS_ORDER): - arguments[argument_name] = positional - positionals.add(argument_name) - - duplicated_kwargs = {kwarg for kwarg in kwargs.keys() if kwarg in positionals} - if duplicated_kwargs: - raise TypeError( - "The fixture arguments are defined as positional and keyword: {}. " - "Use only keyword arguments.".format(", ".join(duplicated_kwargs)) - ) - - if positionals: - warnings.warn(FIXTURE_POSITIONAL_ARGUMENTS, stacklevel=2) - - arguments.update(kwargs) - - return fixture_function, arguments - - def fixture( - callable_or_scope=None, + fixture_function=None, *args, scope="function", params=None, @@ -1131,24 +1089,56 @@ def fixture( ``fixture_`` and then use ``@pytest.fixture(name='')``. """ + # Positional arguments backward compatibility. + # If a kwarg is equal to its default, assume it was not explicitly + # passed, i.e. not duplicated. The more correct way is to use a + # **kwargs and check `in`, but that obfuscates the function signature. + if isinstance(fixture_function, str): + # It's actually the first positional argument, scope. + args = (fixture_function, *args) + fixture_function = None + duplicated_args = [] + if len(args) > 0: + if scope == "function": + scope = args[0] + else: + duplicated_args.append("scope") + if len(args) > 1: + if params is None: + params = args[1] + else: + duplicated_args.append("params") + if len(args) > 2: + if autouse is False: + autouse = args[2] + else: + duplicated_args.append("autouse") + if len(args) > 3: + if ids is None: + ids = args[3] + else: + duplicated_args.append("ids") + if len(args) > 4: + if name is None: + name = args[4] + else: + duplicated_args.append("name") + if len(args) > 5: + raise TypeError( + "fixture() takes 5 positional arguments but {} were given".format(len(args)) + ) + if duplicated_args: + raise TypeError( + "The fixture arguments are defined as positional and keyword: {}. " + "Use only keyword arguments.".format(", ".join(duplicated_args)) + ) + if args: + warnings.warn(FIXTURE_POSITIONAL_ARGUMENTS, stacklevel=2) + # End backward compatiblity. + if params is not None: params = list(params) - fixture_function, arguments = _parse_fixture_args( - callable_or_scope, - *args, - scope=scope, - params=params, - autouse=autouse, - ids=ids, - name=name, - ) - scope = arguments.get("scope") - params = arguments.get("params") - autouse = arguments.get("autouse") - ids = arguments.get("ids") - name = arguments.get("name") - if fixture_function and params is None and autouse is False: # direct decoration return FixtureFunctionMarker(scope, params, autouse, name=name)( @@ -1159,7 +1149,7 @@ def fixture( def yield_fixture( - callable_or_scope=None, + fixture_function=None, *args, scope="function", params=None, @@ -1173,7 +1163,7 @@ def yield_fixture( Use :py:func:`pytest.fixture` directly instead. """ return fixture( - callable_or_scope, + fixture_function, *args, scope=scope, params=params, diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index bfbe359515c..d9130d8440d 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4155,7 +4155,7 @@ def test_fixture_named_request(testdir): ) -def test_fixture_duplicated_arguments(): +def test_fixture_duplicated_arguments() -> None: """Raise error if there are positional and keyword arguments for the same parameter (#1682).""" with pytest.raises(TypeError) as excinfo: @@ -4169,8 +4169,31 @@ def arg(arg): "Use only keyword arguments." ) + with pytest.raises(TypeError) as excinfo: + + @pytest.fixture( + "function", + ["p1"], + True, + ["id1"], + "name", + scope="session", + params=["p1"], + autouse=True, + ids=["id1"], + name="name", + ) + def arg2(request): + pass + + assert ( + str(excinfo.value) + == "The fixture arguments are defined as positional and keyword: scope, params, autouse, ids, name. " + "Use only keyword arguments." + ) -def test_fixture_with_positionals(): + +def test_fixture_with_positionals() -> None: """Raise warning, but the positionals should still works (#1682).""" from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS @@ -4187,6 +4210,18 @@ def fixture_with_positionals(): assert fixture_with_positionals._pytestfixturefunction.autouse +def test_fixture_with_too_many_positionals() -> None: + with pytest.raises(TypeError) as excinfo: + + @pytest.fixture("function", [0], True, ["id"], "name", "extra") + def fixture_with_positionals(): + pass + + assert ( + str(excinfo.value) == "fixture() takes 5 positional arguments but 6 were given" + ) + + def test_indirect_fixture_does_not_break_scope(testdir): """Ensure that fixture scope is respected when using indirect fixtures (#570)""" testdir.makepyfile( From 20f6331afd3cf5af1d708f77aede08ad1a9d8661 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 3 Apr 2020 00:56:53 +0200 Subject: [PATCH 064/823] Fix TerminalRepr instances to be hashable (#6988) pytest-xdist assumes `ExceptionChainRepr` is hashable. Fixes https://github.com/pytest-dev/pytest/issues/6925. Fixes https://github.com/pytest-dev/pytest-xdist/issues/515. --- changelog/6925.bugfix.rst | 1 + src/_pytest/_code/code.py | 21 +++++++++++---------- testing/code/test_code.py | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 changelog/6925.bugfix.rst diff --git a/changelog/6925.bugfix.rst b/changelog/6925.bugfix.rst new file mode 100644 index 00000000000..ed7e99b5dd2 --- /dev/null +++ b/changelog/6925.bugfix.rst @@ -0,0 +1 @@ +Fix TerminalRepr instances to be hashable again. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 996866c04a7..02efc71722b 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -32,6 +32,7 @@ from _pytest._io import TerminalWriter from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr +from _pytest.compat import ATTRS_EQ_FIELD from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING @@ -911,7 +912,7 @@ def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr": return ExceptionChainRepr(repr_chain) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class TerminalRepr: def __str__(self) -> str: # FYI this is called from pytest-xdist's serialization of exception @@ -928,7 +929,7 @@ def toterminal(self, tw: TerminalWriter) -> None: raise NotImplementedError() -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ExceptionRepr(TerminalRepr): def __attrs_post_init__(self): self.sections = [] # type: List[Tuple[str, str, str]] @@ -942,7 +943,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line(content) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ExceptionChainRepr(ExceptionRepr): chain = attr.ib( type=Sequence[ @@ -966,7 +967,7 @@ def toterminal(self, tw: TerminalWriter) -> None: super().toterminal(tw) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprExceptionInfo(ExceptionRepr): reprtraceback = attr.ib(type="ReprTraceback") reprcrash = attr.ib(type="ReprFileLocation") @@ -976,7 +977,7 @@ def toterminal(self, tw: TerminalWriter) -> None: super().toterminal(tw) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprTraceback(TerminalRepr): reprentries = attr.ib(type=Sequence[Union["ReprEntry", "ReprEntryNative"]]) extraline = attr.ib(type=Optional[str]) @@ -1010,7 +1011,7 @@ def __init__(self, tblines: Sequence[str]) -> None: self.extraline = None -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprEntryNative(TerminalRepr): lines = attr.ib(type=Sequence[str]) style = "native" # type: _TracebackStyle @@ -1019,7 +1020,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.write("".join(self.lines)) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprEntry(TerminalRepr): lines = attr.ib(type=Sequence[str]) reprfuncargs = attr.ib(type=Optional["ReprFuncArgs"]) @@ -1100,7 +1101,7 @@ def __str__(self) -> str: ) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprFileLocation(TerminalRepr): path = attr.ib(type=str, converter=str) lineno = attr.ib(type=int) @@ -1117,7 +1118,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line(":{}: {}".format(self.lineno, msg)) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprLocals(TerminalRepr): lines = attr.ib(type=Sequence[str]) @@ -1126,7 +1127,7 @@ def toterminal(self, tw: TerminalWriter, indent="") -> None: tw.line(indent + line) -@attr.s +@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ReprFuncArgs(TerminalRepr): args = attr.ib(type=Sequence[Tuple[str, object]]) diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 826a377089b..5cbd899905b 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -6,6 +6,7 @@ from _pytest._code import Code from _pytest._code import ExceptionInfo from _pytest._code import Frame +from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ReprFuncArgs @@ -180,3 +181,20 @@ def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None: tw_mock.lines[0] == r"unicode_string = São Paulo, utf8_string = b'S\xc3\xa3o Paulo'" ) + + +def test_ExceptionChainRepr(): + """Test ExceptionChainRepr, especially with regard to being hashable.""" + try: + raise ValueError() + except ValueError: + excinfo1 = ExceptionInfo.from_current() + excinfo2 = ExceptionInfo.from_current() + + repr1 = excinfo1.getrepr() + repr2 = excinfo2.getrepr() + assert repr1 != repr2 + + assert isinstance(repr1, ExceptionChainRepr) + assert hash(repr1) != hash(repr2) + assert repr1 is not excinfo1.getrepr() From 48c9f556ef17d67b820ac36944f5ee234dd624c6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 4 Apr 2020 12:33:15 +0200 Subject: [PATCH 065/823] Fix tests: use explicit syspathinsert where tests might hang (#7008) Use `testdir.syspathinsert()` with multiprocessing tests: - test_chained_exceptions_no_reprcrash - test_exception_handling_no_traceback This only works currently because `_importtestmodule` changes `sys.path` as a side-effect. It appears to be only required on Windows though - likely due to the multiprocessing method used there. --- testing/test_assertion.py | 1 + testing/test_reports.py | 1 + 2 files changed, 2 insertions(+) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 3ce0f93e66e..e6e42b6dd87 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1264,6 +1264,7 @@ def test_multitask_job(): multitask_job() """ ) + testdir.syspathinsert() result = testdir.runpytest(p1, "--tb=long") result.stdout.fnmatch_lines( [ diff --git a/testing/test_reports.py b/testing/test_reports.py index 8c509ec479d..13f5932156b 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -362,6 +362,7 @@ def test_a(): """ ) + testdir.syspathinsert() reprec = testdir.inline_run() reports = reprec.getreports("pytest_runtest_logreport") From 4a324ce920ebab04beebd16f7cbb980c6e02e771 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 3 Apr 2020 12:14:36 +0300 Subject: [PATCH 066/823] Remove unused defaultfuncargprefixmarker Unused since 1e80a9cb34c73066cc8fa232be9b20fe284b8ae9. --- src/_pytest/fixtures.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 4c3d0d4bbb5..3111e28f52b 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1183,9 +1183,6 @@ def yield_fixture( ) -defaultfuncargprefixmarker = fixture() - - @fixture(scope="session") def pytestconfig(request): """Session-scoped fixture that returns the :class:`_pytest.config.Config` object. From e01dcbf3235188c64d8aa3b9cdc014efbc29e15d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 4 Apr 2020 14:25:34 +0200 Subject: [PATCH 067/823] Cleanup/move imports with tmpdir tests (#7015) --- testing/test_tmpdir.py | 51 +++++++++++++----------------------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 1c3b32ae490..3316751fb39 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -6,7 +6,17 @@ import pytest from _pytest import pathlib +from _pytest.pathlib import cleanup_numbered_dir +from _pytest.pathlib import create_cleanup_lock +from _pytest.pathlib import make_numbered_dir +from _pytest.pathlib import maybe_delete_a_numbered_dir +from _pytest.pathlib import on_rm_rf_error from _pytest.pathlib import Path +from _pytest.pathlib import register_cleanup_lock_removal +from _pytest.pathlib import rm_rf +from _pytest.tmpdir import get_user +from _pytest.tmpdir import TempdirFactory +from _pytest.tmpdir import TempPathFactory def test_tmpdir_fixture(testdir): @@ -33,9 +43,6 @@ def option(self): class TestTempdirHandler: def test_mktemp(self, tmp_path): - - from _pytest.tmpdir import TempdirFactory, TempPathFactory - config = FakeConfig(tmp_path) t = TempdirFactory(TempPathFactory.from_config(config)) tmp = t.mktemp("world") @@ -48,8 +55,6 @@ def test_mktemp(self, tmp_path): def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch): """#4425""" - from _pytest.tmpdir import TempPathFactory - monkeypatch.chdir(tmp_path) config = FakeConfig("hello") t = TempPathFactory.from_config(config) @@ -91,7 +96,6 @@ def test_mktemp(testdir, basename, is_ok): mytemp = testdir.tmpdir.mkdir("mytemp") p = testdir.makepyfile( """ - import pytest def test_abs_path(tmpdir_factory): tmpdir_factory.mktemp('{}', numbered=False) """.format( @@ -181,7 +185,6 @@ def test_tmpdir_fallback_tox_env(testdir, monkeypatch): monkeypatch.delenv("USERNAME", raising=False) testdir.makepyfile( """ - import pytest def test_some(tmpdir): assert tmpdir.isdir() """ @@ -207,7 +210,6 @@ def test_tmpdir_fallback_uid_not_found(testdir): testdir.makepyfile( """ - import pytest def test_some(tmpdir): assert tmpdir.isdir() """ @@ -223,8 +225,6 @@ def test_get_user_uid_not_found(): user id does not correspond to a valid user (e.g. running pytest in a Docker container with 'docker run -u'. """ - from _pytest.tmpdir import get_user - assert get_user() is None @@ -234,8 +234,6 @@ def test_get_user(monkeypatch): required by getpass module are missing from the environment on Windows (#1010). """ - from _pytest.tmpdir import get_user - monkeypatch.delenv("USER", raising=False) monkeypatch.delenv("USERNAME", raising=False) assert get_user() is None @@ -245,8 +243,6 @@ class TestNumberedDir: PREFIX = "fun-" def test_make(self, tmp_path): - from _pytest.pathlib import make_numbered_dir - for i in range(10): d = make_numbered_dir(root=tmp_path, prefix=self.PREFIX) assert d.name.startswith(self.PREFIX) @@ -261,8 +257,6 @@ def test_make(self, tmp_path): def test_cleanup_lock_create(self, tmp_path): d = tmp_path.joinpath("test") d.mkdir() - from _pytest.pathlib import create_cleanup_lock - lockfile = create_cleanup_lock(d) with pytest.raises(OSError, match="cannot create lockfile in .*"): create_cleanup_lock(d) @@ -270,8 +264,6 @@ def test_cleanup_lock_create(self, tmp_path): lockfile.unlink() def test_lock_register_cleanup_removal(self, tmp_path): - from _pytest.pathlib import create_cleanup_lock, register_cleanup_lock_removal - lock = create_cleanup_lock(tmp_path) registry = [] @@ -295,8 +287,6 @@ def test_lock_register_cleanup_removal(self, tmp_path): def _do_cleanup(self, tmp_path): self.test_make(tmp_path) - from _pytest.pathlib import cleanup_numbered_dir - cleanup_numbered_dir( root=tmp_path, prefix=self.PREFIX, @@ -310,12 +300,9 @@ def test_cleanup_keep(self, tmp_path): print(a, b) def test_cleanup_locked(self, tmp_path): + p = make_numbered_dir(root=tmp_path, prefix=self.PREFIX) - from _pytest import pathlib - - p = pathlib.make_numbered_dir(root=tmp_path, prefix=self.PREFIX) - - pathlib.create_cleanup_lock(p) + create_cleanup_lock(p) assert not pathlib.ensure_deletable( p, consider_lock_dead_if_created_before=p.stat().st_mtime - 1 @@ -330,16 +317,14 @@ def test_cleanup_ignores_symlink(self, tmp_path): self._do_cleanup(tmp_path) def test_removal_accepts_lock(self, tmp_path): - folder = pathlib.make_numbered_dir(root=tmp_path, prefix=self.PREFIX) - pathlib.create_cleanup_lock(folder) - pathlib.maybe_delete_a_numbered_dir(folder) + folder = make_numbered_dir(root=tmp_path, prefix=self.PREFIX) + create_cleanup_lock(folder) + maybe_delete_a_numbered_dir(folder) assert folder.is_dir() class TestRmRf: def test_rm_rf(self, tmp_path): - from _pytest.pathlib import rm_rf - adir = tmp_path / "adir" adir.mkdir() rm_rf(adir) @@ -355,8 +340,6 @@ def test_rm_rf(self, tmp_path): def test_rm_rf_with_read_only_file(self, tmp_path): """Ensure rm_rf can remove directories with read-only files in them (#5524)""" - from _pytest.pathlib import rm_rf - fn = tmp_path / "dir/foo.txt" fn.parent.mkdir() @@ -374,8 +357,6 @@ def chmod_r(self, path): def test_rm_rf_with_read_only_directory(self, tmp_path): """Ensure rm_rf can remove read-only directories (#5524)""" - from _pytest.pathlib import rm_rf - adir = tmp_path / "dir" adir.mkdir() @@ -387,8 +368,6 @@ def test_rm_rf_with_read_only_directory(self, tmp_path): assert not adir.is_dir() def test_on_rm_rf_error(self, tmp_path): - from _pytest.pathlib import on_rm_rf_error - adir = tmp_path / "dir" adir.mkdir() From 1ce30fd38f9e58593f3605a2fcf5a0e9f185cb03 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 4 Apr 2020 14:00:15 +0300 Subject: [PATCH 068/823] Document the pytest_report_teststatus hook better and test uncovered functionality This hook has some functionality to provide explicit markup for the test status. It seemed unused and wasn't tested, so I was tempted to remove it, but I found that the pytest-rerunfailures plugin uses it, so document it and add a test instead. --- src/_pytest/hookspec.py | 33 +++++++++++++++++++++++++++++---- testing/test_terminal.py | 23 +++++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 1e16d092d0b..962ec8b3a8c 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,6 +1,9 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ from typing import Any +from typing import Mapping from typing import Optional +from typing import Tuple +from typing import Union from pluggy import HookspecMarker @@ -8,7 +11,9 @@ from _pytest.compat import TYPE_CHECKING if TYPE_CHECKING: + from _pytest.config import Config from _pytest.main import Session + from _pytest.reports import BaseReport hookspec = HookspecMarker("pytest") @@ -546,12 +551,32 @@ def pytest_report_collectionfinish(config, startdir, items): @hookspec(firstresult=True) -def pytest_report_teststatus(report, config): - """ return result-category, shortletter and verbose word for reporting. +def pytest_report_teststatus( + report: "BaseReport", config: "Config" +) -> Tuple[ + str, str, Union[str, Mapping[str, bool]], +]: + """Return result-category, shortletter and verbose word for status + reporting. - :param _pytest.config.Config config: pytest config object + The result-category is a category in which to count the result, for + example "passed", "skipped", "error" or the empty string. - Stops at first non-None result, see :ref:`firstresult` """ + The shortletter is shown as testing progresses, for example ".", "s", + "E" or the empty string. + + The verbose word is shown as testing progresses in verbose mode, for + example "PASSED", "SKIPPED", "ERROR" or the empty string. + + pytest may style these implicitly according to the report outcome. + To provide explicit styling, return a tuple for the verbose word, + for example ``"rerun", "R", ("RERUN", {"yellow": True})``. + + :param report: The report object whose status is to be returned. + :param _pytest.config.Config config: The pytest config object. + + Stops at first non-None result, see :ref:`firstresult`. + """ def pytest_terminal_summary(terminalreporter, exitstatus, config): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 38ca1957a91..88d564519e0 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -306,6 +306,29 @@ def test_rewrite(self, testdir, monkeypatch): tr.rewrite("hey", erase=True) assert f.getvalue() == "hello" + "\r" + "hey" + (6 * " ") + def test_report_teststatus_explicit_markup( + self, testdir: Testdir, color_mapping + ) -> None: + """Test that TerminalReporter handles markup explicitly provided by + a pytest_report_teststatus hook.""" + testdir.monkeypatch.setenv("PY_COLORS", "1") + testdir.makeconftest( + """ + def pytest_report_teststatus(report): + return 'foo', 'F', ('FOO', {'red': True}) + """ + ) + testdir.makepyfile( + """ + def test_foobar(): + pass + """ + ) + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines( + color_mapping.format_for_fnmatch(["*{red}FOO{reset}*"]) + ) + class TestCollectonly: def test_collectonly_basic(self, testdir): From 51c1ae89cdae01019d2e94118ce91d143043e073 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 6 Apr 2020 07:02:15 +0200 Subject: [PATCH 069/823] doc: remove deprecations from pytest 3 This is in line with 9c5da9c0d, which versionadded instructions. --- doc/en/deprecations.rst | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 732f92985f1..6e853898f77 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -575,40 +575,3 @@ As a stopgap measure, plugin authors may still inject their names into pytest's def pytest_configure(): pytest.my_symbol = MySymbol() - - - - -Reinterpretation mode (``--assert=reinterp``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionremoved:: 3.0 - -Reinterpretation mode has now been removed and only plain and rewrite -mode are available, consequently the ``--assert=reinterp`` option is -no longer available. This also means files imported from plugins or -``conftest.py`` will not benefit from improved assertions by -default, you should use ``pytest.register_assert_rewrite()`` to -explicitly turn on assertion rewriting for those files. - -Removed command-line options -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionremoved:: 3.0 - -The following deprecated commandline options were removed: - -* ``--genscript``: no longer supported; -* ``--no-assert``: use ``--assert=plain`` instead; -* ``--nomagic``: use ``--assert=plain`` instead; -* ``--report``: use ``-r`` instead; - -py.test-X* entry points -~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionremoved:: 3.0 - -Removed all ``py.test-X*`` entry points. The versioned, suffixed entry points -were never documented and a leftover from a pre-virtualenv era. These entry -points also created broken entry points in wheels, so removing them also -removes a source of confusion for users. From fc645412aa0169c8846010770e7cc4e5a32ef670 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 6 Apr 2020 08:30:56 +0200 Subject: [PATCH 070/823] Fix `test_popen_default_stdin_stderr_and_stdin_None` when run with `-s` --- testing/test_pytester.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 959000061a5..b6b1a0b5c70 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -679,19 +679,30 @@ def test_popen_default_stdin_stderr_and_stdin_None(testdir) -> None: # stdout, stderr default to pipes, # stdin can be None to not close the pipe, avoiding # "ValueError: flush of closed file" with `communicate()`. + # + # Wraps the test to not make it hang when run with "-s". p1 = testdir.makepyfile( - """ + ''' import sys - print(sys.stdin.read()) # empty - print('stdout') - sys.stderr.write('stderr') - """ + + def test_inner(testdir): + p1 = testdir.makepyfile( + """ + import sys + print(sys.stdin.read()) # empty + print('stdout') + sys.stderr.write('stderr') + """ + ) + proc = testdir.popen([sys.executable, str(p1)], stdin=None) + stdout, stderr = proc.communicate(b"ignored") + assert stdout.splitlines() == [b"", b"stdout"] + assert stderr.splitlines() == [b"stderr"] + assert proc.returncode == 0 + ''' ) - proc = testdir.popen([sys.executable, str(p1)], stdin=None) - stdout, stderr = proc.communicate(b"ignored") - assert stdout.splitlines() == [b"", b"stdout"] - assert stderr.splitlines() == [b"stderr"] - assert proc.returncode == 0 + result = testdir.runpytest("-p", "pytester", str(p1)) + assert result.ret == 0 def test_spawn_uses_tmphome(testdir) -> None: From 7da3e3aaad89dabd790f2cccf31c1a3f2fbfa01f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 5 Apr 2020 18:22:01 +0300 Subject: [PATCH 071/823] Increase test_faulthandler.py::test_timeout sleep duration on CI This might help fix some flakiness. --- testing/test_faulthandler.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index 7580f6f2fc1..f4c190ac42e 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -58,7 +58,7 @@ def test_timeout(testdir, enabled): """ import os, time def test_timeout(): - time.sleep(1 if "CI" in os.environ else 0.1) + time.sleep(10 if "CI" in os.environ else 0.1) """ ) testdir.makeini( @@ -71,8 +71,6 @@ def test_timeout(): result = testdir.runpytest_subprocess(*args) tb_output = "most recent call first" - if sys.version_info[:2] == (3, 3): - tb_output = "Thread" if enabled: result.stderr.fnmatch_lines(["*%s*" % tb_output]) else: From c3e6e2e8c8ee3b0f609d3218b6ef2ce704aecf1b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 6 Apr 2020 23:05:15 +0300 Subject: [PATCH 072/823] Fix flaky TestDurations test TestDurations tests the `--durations=N` functionality which reports N slowest tests, with durations <= 0.005s not shown by default. The test relies on real time.sleep() (in addition to the code which uses time.perf_counter()) which makes it flaky and inconsistent between platforms. Instead of trying to tweak it more, make it use fake time instead. The way it is done is a little hacky but seems to work. --- src/_pytest/runner.py | 4 +- testing/acceptance_test.py | 85 +++++++++++++++++++++----------------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index c13dff711a1..f87ccb461ed 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -2,8 +2,8 @@ import bdb import os import sys -from time import perf_counter -from time import time +from time import perf_counter # Intentionally not `import time` to avoid being +from time import time # affected by tests which monkeypatch `time` (issue #185). from typing import Callable from typing import Dict from typing import List diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c28c35b8621..0ac3be12205 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -6,9 +6,11 @@ import attr import py +import _pytest.runner import pytest from _pytest.compat import importlib_metadata from _pytest.config import ExitCode +from _pytest.monkeypatch import MonkeyPatch def prepend_pythonpath(*dirs): @@ -893,61 +895,64 @@ def test_has_plugin(self, request): assert request.config.pluginmanager.hasplugin("python") +def fake_time(monkeypatch: MonkeyPatch) -> None: + """Monkeypatch time functions to make TestDurations not rely on actual time.""" + import time + + current_time = 1586202699.9859412 + + def sleep(seconds: float) -> None: + nonlocal current_time + current_time += seconds + + monkeypatch.setattr(time, "sleep", sleep) + monkeypatch.setattr(_pytest.runner, "time", lambda: current_time) + monkeypatch.setattr(_pytest.runner, "perf_counter", lambda: current_time) + + class TestDurations: source = """ import time - frag = 0.002 # 2 ms def test_something(): pass def test_2(): - time.sleep(frag*5) # 10 ms: on windows might sleep < 0.005s + time.sleep(0.010) def test_1(): - time.sleep(frag) # 2 ms: on macOS/windows might sleep > 0.005s + time.sleep(0.002) def test_3(): - time.sleep(frag*10) # 20 ms + time.sleep(0.020) """ def test_calls(self, testdir): testdir.makepyfile(self.source) - result = testdir.runpytest("--durations=10") + fake_time(testdir.monkeypatch) + result = testdir.runpytest_inprocess("--durations=10") assert result.ret == 0 - # on Windows, test 2 (10ms) can actually sleep less than 5ms and become hidden - if sys.platform == "win32": - to_match = ["*durations*", "*call*test_3*"] - else: - to_match = ["*durations*", "*call*test_3*", "*call*test_2*"] - result.stdout.fnmatch_lines_random(to_match) - - # The number of hidden should be 8, but on macOS and windows it sometimes is 7 - # - on MacOS and Windows test 1 can last longer and appear in the list - # - on Windows test 2 can last less and disappear from the list - if sys.platform in ("win32", "darwin"): - nb_hidden = "*" - else: - nb_hidden = "8" + result.stdout.fnmatch_lines_random( + ["*durations*", "*call*test_3*", "*call*test_2*"] + ) result.stdout.fnmatch_lines( - [ - "(%s durations < 0.005s hidden. Use -vv to show these durations.)" - % nb_hidden - ] + ["(8 durations < 0.005s hidden. Use -vv to show these durations.)"] ) def test_calls_show_2(self, testdir): testdir.makepyfile(self.source) - result = testdir.runpytest("--durations=2") + fake_time(testdir.monkeypatch) + result = testdir.runpytest_inprocess("--durations=2") assert result.ret == 0 + lines = result.stdout.get_lines_after("*slowest*durations*") assert "4 passed" in lines[2] def test_calls_showall(self, testdir): testdir.makepyfile(self.source) - result = testdir.runpytest("--durations=0") + fake_time(testdir.monkeypatch) + result = testdir.runpytest_inprocess("--durations=0") assert result.ret == 0 - # on windows, test 2 (10ms) can actually sleep less than 5ms and become hidden - tested = "3" if sys.platform == "win32" else "23" + tested = "3" for x in tested: for y in ("call",): # 'setup', 'call', 'teardown': for line in result.stdout.lines: @@ -958,8 +963,10 @@ def test_calls_showall(self, testdir): def test_calls_showall_verbose(self, testdir): testdir.makepyfile(self.source) - result = testdir.runpytest("--durations=0", "-vv") + fake_time(testdir.monkeypatch) + result = testdir.runpytest_inprocess("--durations=0", "-vv") assert result.ret == 0 + for x in "123": for y in ("call",): # 'setup', 'call', 'teardown': for line in result.stdout.lines: @@ -970,16 +977,19 @@ def test_calls_showall_verbose(self, testdir): def test_with_deselected(self, testdir): testdir.makepyfile(self.source) - # on windows test 2 might sleep less than 0.005s and be hidden. Prefer test 3. - result = testdir.runpytest("--durations=2", "-k test_3") + fake_time(testdir.monkeypatch) + result = testdir.runpytest_inprocess("--durations=2", "-k test_3") assert result.ret == 0 + result.stdout.fnmatch_lines(["*durations*", "*call*test_3*"]) def test_with_failing_collection(self, testdir): testdir.makepyfile(self.source) + fake_time(testdir.monkeypatch) testdir.makepyfile(test_collecterror="""xyz""") - result = testdir.runpytest("--durations=2", "-k test_1") + result = testdir.runpytest_inprocess("--durations=2", "-k test_1") assert result.ret == 2 + result.stdout.fnmatch_lines(["*Interrupted: 1 error during collection*"]) # Collection errors abort test execution, therefore no duration is # output @@ -987,27 +997,28 @@ def test_with_failing_collection(self, testdir): def test_with_not(self, testdir): testdir.makepyfile(self.source) - result = testdir.runpytest("-k not 1") + fake_time(testdir.monkeypatch) + result = testdir.runpytest_inprocess("-k not 1") assert result.ret == 0 -class TestDurationWithFixture: +class TestDurationsWithFixture: source = """ import pytest import time - frag = 0.02 # as on windows sleep(0.01) might take < 0.005s @pytest.fixture def setup_fixt(): - time.sleep(frag) + time.sleep(0.02) def test_1(setup_fixt): - time.sleep(frag) + time.sleep(0.02) """ def test_setup_function(self, testdir): testdir.makepyfile(self.source) - result = testdir.runpytest("--durations=10") + fake_time(testdir.monkeypatch) + result = testdir.runpytest_inprocess("--durations=10") assert result.ret == 0 result.stdout.fnmatch_lines_random( From 3a4435fb59604d40c5d2e2f65e9acba99dd9cff0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 7 Apr 2020 08:01:50 +0200 Subject: [PATCH 073/823] doc: fix/remove leftovers from removing `versionadded` (#7028) Ref: 9c5da9c (https://github.com/pytest-dev/pytest/pull/5184) --- doc/en/assert.rst | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/doc/en/assert.rst b/doc/en/assert.rst index d7c380c60a1..5ece98e963d 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -342,15 +342,3 @@ If this is the case you have two options: ``PYTEST_DONT_REWRITE`` to its docstring. * Disable rewriting for all modules by using ``--assert=plain``. - - - - Add assert rewriting as an alternate introspection technique. - - - Introduce the ``--assert`` option. Deprecate ``--no-assert`` and - ``--nomagic``. - - - Removes the ``--no-assert`` and ``--nomagic`` options. - Removes the ``--assert=reinterp`` option. From bf3e64d473f5ee1b05f18d61de7affcfc3f55687 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 7 Apr 2020 08:07:20 +0200 Subject: [PATCH 074/823] Remove TestExecutionForked - xdist.boxed has gone since long (#7021) Tried to write a test using `--boxed`, but it fails due to https://github.com/pytest-dev/pytest-forked/issues/30. --- testing/test_runner.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/testing/test_runner.py b/testing/test_runner.py index ab4ae67e566..00732d03b26 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -423,27 +423,6 @@ def test_func(): assert False, "did not raise" -class TestExecutionForked(BaseFunctionalTests): - pytestmark = pytest.mark.skipif("not hasattr(os, 'fork')") - - def getrunner(self): - # XXX re-arrange this test to live in pytest-xdist - boxed = pytest.importorskip("xdist.boxed") - return boxed.forked_run_report - - def test_suicide(self, testdir) -> None: - reports = testdir.runitem( - """ - def test_func(): - import os - os.kill(os.getpid(), 15) - """ - ) - rep = reports[0] - assert rep.failed - assert rep.when == "???" - - class TestSessionReports: def test_collect_result(self, testdir) -> None: col = testdir.getmodulecol( From 1fd14685c5860b248c51c9a39de3595ab62f45da Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 7 Apr 2020 08:08:28 +0200 Subject: [PATCH 075/823] doc: document inversed lines with terminal report hooks (#7016) It was surprising that `tryfirst=True` would not result in lines being added to the beginning with `pytest_report_header`. This is due to lines being reversed, and therefore the same applies to `pytest_report_collectionfinish`. --- doc/en/writing_plugins.rst | 1 + src/_pytest/hookspec.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index accda3634cf..87d81edb09f 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -561,6 +561,7 @@ that result but it's probably better to avoid it. For more information, consult the :ref:`pluggy documentation about hookwrappers `. +.. _plugin-hookorder: Hook function ordering / call example ------------------------------------- diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 962ec8b3a8c..210cf22552f 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -528,6 +528,13 @@ def pytest_report_header(config, startdir): :param _pytest.config.Config config: pytest config object :param startdir: py.path object with the starting dir + .. note:: + + Lines returned by a plugin are displayed before those of plugins which + ran before it. + If you want to have your line(s) displayed first, use + :ref:`trylast=True `. + .. note:: This function should be implemented only in plugins or ``conftest.py`` @@ -542,11 +549,18 @@ def pytest_report_collectionfinish(config, startdir, items): return a string or list of strings to be displayed after collection has finished successfully. - This strings will be displayed after the standard "collected X items" message. + These strings will be displayed after the standard "collected X items" message. :param _pytest.config.Config config: pytest config object :param startdir: py.path object with the starting dir :param items: list of pytest items that are going to be executed; this list should not be modified. + + .. note:: + + Lines returned by a plugin are displayed before those of plugins which + ran before it. + If you want to have your line(s) displayed first, use + :ref:`trylast=True `. """ From ce806001b0bc93d37faa3b255e907fb3d0a9bef6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 7 Apr 2020 11:44:54 +0200 Subject: [PATCH 076/823] Fix doc for `numbered` arg with `TempPathFactory.mktemp` (#7014) --- src/_pytest/tmpdir.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index c1e12da4f0d..ff3944dba82 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -59,8 +59,8 @@ def mktemp(self, basename: str, numbered: bool = True) -> Path: Directory base name, must be a relative path. :param numbered: - If True, ensure the directory is unique by adding a number - prefix greater than any existing one: ``basename="foo"`` and ``numbered=True`` + If ``True``, ensure the directory is unique by adding a numbered + suffix greater than any existing one: ``basename="foo-"`` and ``numbered=True`` means that this function will create directories named ``"foo-0"``, ``"foo-1"``, ``"foo-2"`` and so on. From eab0a6e34dc0f8c545c47011ff02004e5ff082f4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 7 Apr 2020 10:49:10 +0300 Subject: [PATCH 077/823] Revert "Increase test_faulthandler.py::test_timeout sleep duration on CI" --- testing/test_faulthandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index f4c190ac42e..f323edeb42b 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -58,7 +58,7 @@ def test_timeout(testdir, enabled): """ import os, time def test_timeout(): - time.sleep(10 if "CI" in os.environ else 0.1) + time.sleep(1 if "CI" in os.environ else 0.1) """ ) testdir.makeini( From 4344c617310df932aa88317ed35cca413b6de402 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 7 Apr 2020 21:59:14 +0300 Subject: [PATCH 078/823] Slightly improve Mark and MarkDecorator documentation Mostly I wanted to get rid of mentions of "MarkItem" which is something that no longer exists, but I improved a little beyond that and annotated some simple types. --- src/_pytest/mark/structures.py | 125 ++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 56 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 1ab22b7c758..bcbfbd72ece 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -2,10 +2,14 @@ import warnings from collections import namedtuple from collections.abc import MutableMapping +from typing import Any from typing import Iterable from typing import List +from typing import Mapping from typing import Optional +from typing import Sequence from typing import Set +from typing import Tuple from typing import Union import attr @@ -140,28 +144,32 @@ def _for_parametrize(cls, argnames, argvalues, func, config, function_definition @attr.s(frozen=True) class Mark: - #: name of the mark + #: Name of the mark. name = attr.ib(type=str) - #: positional arguments of the mark decorator - args = attr.ib() # List[object] - #: keyword arguments of the mark decorator - kwargs = attr.ib() # Dict[str, object] + #: Positional arguments of the mark decorator. + args = attr.ib(type=Tuple[Any, ...]) + #: Keyword arguments of the mark decorator. + kwargs = attr.ib(type=Mapping[str, Any]) - #: source Mark for ids with parametrize Marks + #: Source Mark for ids with parametrize Marks. _param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False) - #: resolved/generated ids with parametrize Marks - _param_ids_generated = attr.ib(type=Optional[List[str]], default=None, repr=False) + #: Resolved/generated ids with parametrize Marks. + _param_ids_generated = attr.ib( + type=Optional[Sequence[str]], default=None, repr=False + ) - def _has_param_ids(self): + def _has_param_ids(self) -> bool: return "ids" in self.kwargs or len(self.args) >= 4 def combined_with(self, other: "Mark") -> "Mark": - """ - :param other: the mark to combine with + """Return a new Mark which is a combination of this + Mark and another Mark. + + Combines by appending args and merging kwargs. + + :param other: The mark to combine with. :type other: Mark :rtype: Mark - - combines by appending args and merging the mappings """ assert self.name == other.name @@ -183,11 +191,12 @@ def combined_with(self, other: "Mark") -> "Mark": @attr.s class MarkDecorator: - """ A decorator for test functions and test classes. When applied - it will create :class:`Mark` objects which are often created like this:: + """A decorator for applying a mark on test functions and classes. + + MarkDecorators are created with ``pytest.mark``:: - mark1 = pytest.mark.NAME # simple MarkDecorator - mark2 = pytest.mark.NAME(name1=value) # parametrized MarkDecorator + mark1 = pytest.mark.NAME # Simple MarkDecorator + mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator and can then be applied as decorators to test functions:: @@ -195,64 +204,64 @@ class MarkDecorator: def test_function(): pass - When a MarkDecorator instance is called it does the following: + When a MarkDecorator is called it does the following: 1. If called with a single class as its only positional argument and no - additional keyword arguments, it attaches itself to the class so it + additional keyword arguments, it attaches the mark to the class so it gets applied automatically to all test cases found in that class. - 2. If called with a single function as its only positional argument and - no additional keyword arguments, it attaches a MarkInfo object to the - function, containing all the arguments already stored internally in - the MarkDecorator. - 3. When called in any other case, it performs a 'fake construction' call, - i.e. it returns a new MarkDecorator instance with the original - MarkDecorator's content updated with the arguments passed to this - call. - - Note: The rules above prevent MarkDecorator objects from storing only a - single function or class reference as their positional argument with no - additional keyword or positional arguments. + 2. If called with a single function as its only positional argument and + no additional keyword arguments, it attaches the mark to the function, + containing all the arguments already stored internally in the + MarkDecorator. + + 3. When called in any other case, it returns a new MarkDecorator instance + with the original MarkDecorator's content updated with the arguments + passed to this call. + + Note: The rules above prevent MarkDecorators from storing only a single + function or class reference as their positional argument with no + additional keyword or positional arguments. You can work around this by + using `with_args()`. """ - mark = attr.ib(validator=attr.validators.instance_of(Mark)) + mark = attr.ib(type=Mark, validator=attr.validators.instance_of(Mark)) @property - def name(self): - """alias for mark.name""" + def name(self) -> str: + """Alias for mark.name.""" return self.mark.name @property - def args(self): - """alias for mark.args""" + def args(self) -> Tuple[Any, ...]: + """Alias for mark.args.""" return self.mark.args @property - def kwargs(self): - """alias for mark.kwargs""" + def kwargs(self) -> Mapping[str, Any]: + """Alias for mark.kwargs.""" return self.mark.kwargs @property - def markname(self): + def markname(self) -> str: return self.name # for backward-compat (2.4.1 had this attr) - def __repr__(self): + def __repr__(self) -> str: return "".format(self.mark) - def with_args(self, *args, **kwargs): - """ return a MarkDecorator with extra arguments added + def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": + """Return a MarkDecorator with extra arguments added. - unlike call this can be used even if the sole argument is a callable/class + Unlike calling the MarkDecorator, with_args() can be used even + if the sole argument is a callable/class. :return: MarkDecorator """ - mark = Mark(self.name, args, kwargs) return self.__class__(self.mark.combined_with(mark)) - def __call__(self, *args, **kwargs): - """ if passed a single callable argument: decorate it with mark info. - otherwise add *args/**kwargs in-place to mark information. """ + def __call__(self, *args: object, **kwargs: object): + """Call the MarkDecorator.""" if args and not kwargs: func = args[0] is_class = inspect.isclass(func) @@ -288,27 +297,31 @@ def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List return [x for x in extracted if isinstance(x, Mark)] -def store_mark(obj, mark): - """store a Mark on an object - this is used to implement the Mark declarations/decorators correctly +def store_mark(obj, mark: Mark) -> None: + """Store a Mark on an object. + + This is used to implement the Mark declarations/decorators correctly. """ assert isinstance(mark, Mark), mark - # always reassign name to avoid updating pytestmark - # in a reference that was only borrowed + # Always reassign name to avoid updating pytestmark in a reference that + # was only borrowed. obj.pytestmark = get_unpacked_marks(obj) + [mark] class MarkGenerator: - """ Factory for :class:`MarkDecorator` objects - exposed as - a ``pytest.mark`` singleton instance. Example:: + """Factory for :class:`MarkDecorator` objects - exposed as + a ``pytest.mark`` singleton instance. + + Example:: import pytest + @pytest.mark.slowtest def test_function(): pass - will set a 'slowtest' :class:`MarkInfo` object - on the ``test_function`` object. """ + applies a 'slowtest' :class:`Mark` on ``test_function``. + """ _config = None _markers = set() # type: Set[str] From 4fd2623c1275ab34e750513bff4367f190291d34 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 8 Apr 2020 18:11:04 +0200 Subject: [PATCH 079/823] tests: fix TypeErrors (#7038) * tests: fix TypeError with test_mark_closest It fails when trying to run it actually: > TypeError: test_has_inherited() takes 0 positional arguments but 1 was given * Fix testing/test_collection.py::TestCollector::test_getparent --- testing/test_collection.py | 4 ++-- testing/test_mark.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/test_collection.py b/testing/test_collection.py index 90c248b4ab2..050b5459812 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -53,8 +53,8 @@ def test_fail(): assert 0 def test_getparent(self, testdir): modcol = testdir.getmodulecol( """ - class TestClass(object): - def test_foo(): + class TestClass: + def test_foo(self): pass """ ) diff --git a/testing/test_mark.py b/testing/test_mark.py index 76ee289b674..530f9f1688c 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -533,10 +533,10 @@ def test_mark_closest(self, testdir): @pytest.mark.c(location="class") class Test: @pytest.mark.c(location="function") - def test_has_own(): + def test_has_own(self): pass - def test_has_inherited(): + def test_has_inherited(self): pass """ From 7048d5be9cd78b17baeecfdb4c278e3ddcac6a51 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 8 Apr 2020 18:11:31 +0200 Subject: [PATCH 080/823] Fix FD leak in test__get_multicapture (#7037) Instantiating `FDCapture` creates a file descriptor already. Use "no" to not leak a fd here. --- testing/test_capture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index 49269ee96de..132ce1cc645 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1489,7 +1489,7 @@ def test_encodedfile_writelines(tmpfile: BinaryIO) -> None: def test__get_multicapture() -> None: - assert isinstance(_get_multicapture("fd"), MultiCapture) + assert isinstance(_get_multicapture("no"), MultiCapture) pytest.raises(ValueError, _get_multicapture, "unknown").match( r"^unknown capturing method: 'unknown'" ) From e6da086101009b51e79fa22c92d9ccdd45604f8e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 8 Apr 2020 18:16:03 +0200 Subject: [PATCH 081/823] Update testing/test_pytester.py Co-Authored-By: Ran Benita --- testing/test_pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index b6b1a0b5c70..fa0cfce9722 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -680,7 +680,7 @@ def test_popen_default_stdin_stderr_and_stdin_None(testdir) -> None: # stdin can be None to not close the pipe, avoiding # "ValueError: flush of closed file" with `communicate()`. # - # Wraps the test to not make it hang when run with "-s". + # Wraps the test to make it not hang when run with "-s". p1 = testdir.makepyfile( ''' import sys From 33ff86343016a0e5f5cceb623c7ea0c3f4ff4157 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 8 Apr 2020 19:24:20 +0200 Subject: [PATCH 082/823] rebuild From 20956b2f4e163ef0180a15082ded8d0acdf76dfc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 8 Apr 2020 21:33:59 +0300 Subject: [PATCH 083/823] Remove some no-longer-needed compat code in test_assertion --- testing/test_assertion.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index e6e42b6dd87..042aa705502 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1,4 +1,4 @@ -import collections.abc as collections_abc +import collections.abc import sys import textwrap from typing import Any @@ -623,11 +623,8 @@ def test_frozenzet(self): assert len(expl) > 1 def test_Sequence(self): - if not hasattr(collections_abc, "MutableSequence"): - pytest.skip("cannot import MutableSequence") - MutableSequence = collections_abc.MutableSequence - - class TestSequence(MutableSequence): # works with a Sequence subclass + # Test comparing with a Sequence subclass. + class TestSequence(collections.abc.MutableSequence): def __init__(self, iterable): self.elements = list(iterable) From ad4c1071d9c64627162d9a761329c4a5c44d0e29 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 9 Apr 2020 01:39:24 +0200 Subject: [PATCH 084/823] doc: internal: remove references to old "newinterpret" module This has been merged into the (only) assertrewrite mode. --- src/_pytest/assertion/__init__.py | 2 +- src/_pytest/freeze_support.py | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index ee7fa6a3af0..7b5a5889d6e 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -114,7 +114,7 @@ def pytest_collection(session: "Session") -> None: def pytest_runtest_protocol(item): """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks - The newinterpret and rewrite modules will use util._reprcompare if + The rewrite module will use util._reprcompare if it exists to use custom reporting via the pytest_assertrepr_compare hook. This sets up this custom comparison for the test. diff --git a/src/_pytest/freeze_support.py b/src/_pytest/freeze_support.py index f9d613a2b64..9d35d9afc60 100644 --- a/src/_pytest/freeze_support.py +++ b/src/_pytest/freeze_support.py @@ -21,13 +21,10 @@ def _iter_all_modules(package, prefix=""): """ Iterates over the names of all modules that can be found in the given package, recursively. - Example: - _iter_all_modules(_pytest) -> - ['_pytest.assertion.newinterpret', - '_pytest.capture', - '_pytest.core', - ... - ] + + >>> import _pytest + >>> list(_iter_all_modules(_pytest)) + ['_pytest._argcomplete', '_pytest._code.code', ...] """ import os import pkgutil From a98a62723e3b0b098d8e510c07c7878f9c7948ea Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 9 Apr 2020 01:57:46 +0200 Subject: [PATCH 085/823] revisit --- src/_pytest/hookspec.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 5edec31d535..0d552088a99 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -170,28 +170,22 @@ def pytest_load_initial_conftests(early_config, parser, args): def pytest_collection(session: "Session") -> Optional[Any]: """Perform the collection protocol for the given session. - Usually plugins will implement this hook only to perform some action before - collection, for example the terminal plugin will use this to start displaying - the collection counter, so usually plugins return `None` from this hook after - performing the desired operation. - - However a plugin might decide to override the collection completely, - in which case it should return `True`, but then it is usually expected - that this hook will also need to perform the following operations - that are usually part of the collection process: - + Stops at first non-None result, see :ref:`firstresult`. + The return value is not used, but only stops further processing. + + The hook is meant to set `session.items` to a sequence of items at least, + but normally should follow this procedure: + 1. Call the pytest_collectstart hook. 2. Call the pytest_collectreport hook. 3. Call the pytest_collection_modifyitems hook. 4. Call the pytest_collection_finish hook. 5. Set session.testscollected to the amount of collect items. 6. Set `session.items` to a list of items. - - If a plugin just wants to skip collection entirely, like `pytest-xdist` - does for master nodes, then it is OK to not do anything other than - returning `True` from here. - Stops at first non-None result, see :ref:`firstresult`. + You can implement this hook to only perform some action before collection, + for example the terminal plugin uses it to start displaying the collection + counter (and returns `None`). :param _pytest.main.Session session: the pytest session object """ From 413ca8a4d097ed1a98b2d1012ca7df17aa6837b1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 9 Apr 2020 08:53:35 +0200 Subject: [PATCH 086/823] faulthandler: trylast=True (#7025) It should happen as late as possible before the test runs. Ref: https://github.com/pytest-dev/pytest/issues/7022 --- src/_pytest/faulthandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 8d723c206cb..4ca903146a6 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -80,7 +80,7 @@ def _get_stderr_fileno(): def get_timeout_config_value(config): return float(config.getini("faulthandler_timeout") or 0.0) - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(hookwrapper=True, trylast=True) def pytest_runtest_protocol(self, item): timeout = self.get_timeout_config_value(item.config) stderr = item.config._store[fault_handler_stderr_key] From 8fab3dd42fa0c9fbce378089b60935046bf9a4d1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 8 Apr 2020 22:35:36 +0300 Subject: [PATCH 087/823] Remove broken _reprcompare disabling fixture in test_assertrewrite.py The `_pytest._code._reprcompare` that was referred to previously doesn't exist -- it was moved to other places but wasn't updated. This regressed in f423ce9c016aff0d84c4a68f6d972833d032181e. Now we don't want it anymore, so keep the status quo by explicitly removing them. --- testing/test_assertrewrite.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 04e0c6f9e94..8cf4929662e 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -25,16 +25,6 @@ from _pytest.pathlib import Path -def setup_module(mod): - mod._old_reprcompare = util._reprcompare - _pytest._code._reprcompare = None - - -def teardown_module(mod): - util._reprcompare = mod._old_reprcompare - del mod._old_reprcompare - - def rewrite(src): tree = ast.parse(src) rewrite_asserts(tree, src.encode()) From 08b3d3717761fa5bd9b1a85ddff444aacec15c32 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 9 Apr 2020 17:11:18 +0300 Subject: [PATCH 088/823] Remove Python 2 compat check in test_monkeypatch.py Presumably it used to test old-style vs. new-style classes, but in the Python 3 conversion SampleNew and SampleOld became the same. --- testing/test_monkeypatch.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index eee8baf3a69..8c2fceb3fc2 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -4,8 +4,12 @@ import textwrap import pytest +from _pytest.compat import TYPE_CHECKING from _pytest.monkeypatch import MonkeyPatch +if TYPE_CHECKING: + from typing import Type + @pytest.fixture def mp(): @@ -331,33 +335,20 @@ def test_importerror(monkeypatch): ) -class SampleNew: - @staticmethod - def hello(): - return True - - -class SampleNewInherit(SampleNew): - pass - - -class SampleOld: - # oldstyle on python2 +class Sample: @staticmethod - def hello(): + def hello() -> bool: return True -class SampleOldInherit(SampleOld): +class SampleInherit(Sample): pass @pytest.mark.parametrize( - "Sample", - [SampleNew, SampleNewInherit, SampleOld, SampleOldInherit], - ids=["new", "new-inherit", "old", "old-inherit"], + "Sample", [Sample, SampleInherit], ids=["new", "new-inherit"], ) -def test_issue156_undo_staticmethod(Sample): +def test_issue156_undo_staticmethod(Sample: "Type[Sample]") -> None: monkeypatch = MonkeyPatch() monkeypatch.setattr(Sample, "hello", None) From 5a5fd01ebe20ec7a500278619af00f288b6310ed Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 9 Apr 2020 17:23:22 +0300 Subject: [PATCH 089/823] Skip flaky test test_faulthandler.py::test_timeout[True] It occasionally crashes on CI, the reason seems out of our control, or at least we can't figure it out. --- testing/test_faulthandler.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index f323edeb42b..46adccd2159 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -49,8 +49,16 @@ def test_disabled(): assert result.ret == 0 -@pytest.mark.parametrize("enabled", [True, False]) -def test_timeout(testdir, enabled): +@pytest.mark.parametrize( + "enabled", + [ + pytest.param( + True, marks=pytest.mark.skip(reason="sometimes crashes on CI (#7022)") + ), + False, + ], +) +def test_timeout(testdir, enabled: bool) -> None: """Test option to dump tracebacks after a certain timeout. If faulthandler is disabled, no traceback will be dumped. """ From 2aa5436ee743bd16fe51d52f6e6c9d05fd0f00bb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 9 Apr 2020 16:36:59 +0300 Subject: [PATCH 090/823] Remove Python 2 compat code in test_juintxml.py --- testing/test_junitxml.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 0d6adb3a063..a1f86b0b85f 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -7,6 +7,7 @@ import xmlschema import pytest +from _pytest.junitxml import bin_xml_escape from _pytest.junitxml import LogXML from _pytest.pathlib import Path from _pytest.reports import BaseReport @@ -969,11 +970,6 @@ def test_invalid_xml_escape(): # the higher ones. # XXX Testing 0xD (\r) is tricky as it overwrites the just written # line in the output, so we skip it too. - global unichr - try: - unichr(65) - except NameError: - unichr = chr invalid = ( 0x00, 0x1, @@ -990,17 +986,15 @@ def test_invalid_xml_escape(): valid = (0x9, 0xA, 0x20) # 0xD, 0xD7FF, 0xE000, 0xFFFD, 0x10000, 0x10FFFF) - from _pytest.junitxml import bin_xml_escape - for i in invalid: - got = bin_xml_escape(unichr(i)).uniobj + got = bin_xml_escape(chr(i)).uniobj if i <= 0xFF: expected = "#x%02X" % i else: expected = "#x%04X" % i assert got == expected for i in valid: - assert chr(i) == bin_xml_escape(unichr(i)).uniobj + assert chr(i) == bin_xml_escape(chr(i)).uniobj def test_logxml_path_expansion(tmpdir, monkeypatch): From f136b79f1a5df99753fd4e0c5290fcee1eb45f47 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 9 Apr 2020 16:56:01 +0200 Subject: [PATCH 091/823] Fix test_no_warnings to handle e.g. `_pytest.async` (#7044) Before this patch it would result in a SyntaxError with e.g. `import _pytest.async`. --- testing/test_meta.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/testing/test_meta.py b/testing/test_meta.py index ffc8fd38aba..7ab8951a015 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -7,29 +7,29 @@ import pkgutil import subprocess import sys +from typing import List import _pytest import pytest -def _modules(): +def _modules() -> List[str]: + pytest_pkg = _pytest.__path__ # type: str # type: ignore return sorted( n - for _, n, _ in pkgutil.walk_packages( - _pytest.__path__, prefix=_pytest.__name__ + "." - ) + for _, n, _ in pkgutil.walk_packages(pytest_pkg, prefix=_pytest.__name__ + ".") ) @pytest.mark.slow @pytest.mark.parametrize("module", _modules()) -def test_no_warnings(module): +def test_no_warnings(module: str) -> None: # fmt: off subprocess.check_call(( sys.executable, "-W", "error", # https://github.com/pytest-dev/pytest/issues/5901 "-W", "ignore:The usage of `cmp` is deprecated and will be removed on or after 2021-06-01. Please use `eq` and `order` instead.:DeprecationWarning", # noqa: E501 - "-c", "import {}".format(module), + "-c", "__import__({!r})".format(module), )) # fmt: on From accea46fa13aa8b77ac102fb99079d195cdd9131 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 9 Apr 2020 17:08:47 +0200 Subject: [PATCH 092/823] doc: minor fixes for Store --- src/_pytest/store.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/store.py b/src/_pytest/store.py index eed50d103aa..2b46c438936 100644 --- a/src/_pytest/store.py +++ b/src/_pytest/store.py @@ -27,7 +27,7 @@ class StoreKey(Generic[T]): class Store: """Store is a type-safe heterogenous mutable mapping that allows keys and value types to be defined separately from - where it is defined. + where it (the Store) is created. Usually you will be given an object which has a ``Store``: @@ -77,7 +77,7 @@ class Store: Good solution: module Internal adds a ``Store`` to the object. Module External mints StoreKeys for its own keys. Module External stores and - retrieves its data using its keys. + retrieves its data using these keys. """ __slots__ = ("_store",) From 0b5d2ff526fdff018459ad971da33ca8114b3d80 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 9 Apr 2020 21:47:12 +0200 Subject: [PATCH 093/823] Fix/improve printing of docs for collected items --- src/_pytest/terminal.py | 12 +++++++++--- testing/test_terminal.py | 28 ++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index a99463fe875..1273e0cd979 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -5,6 +5,7 @@ import argparse import collections import datetime +import inspect import platform import sys import time @@ -707,9 +708,14 @@ def _printcollecteditems(self, items): indent = (len(stack) - 1) * " " self._tw.line("{}{}".format(indent, col)) if self.config.option.verbose >= 1: - if hasattr(col, "_obj") and col._obj.__doc__: - for line in col._obj.__doc__.strip().splitlines(): - self._tw.line("{}{}".format(indent + " ", line.strip())) + try: + obj = col.obj # type: ignore + except AttributeError: + continue + doc = inspect.getdoc(obj) + if doc: + for line in doc.splitlines(): + self._tw.line("{}{}".format(indent + " ", line)) @pytest.hookimpl(hookwrapper=True) def pytest_sessionfinish(self, session: Session, exitstatus: ExitCode): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 88d564519e0..72c67cc3930 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -353,17 +353,33 @@ def test_collectonly_skipped_module(self, testdir): result = testdir.runpytest("--collect-only", "-rs") result.stdout.fnmatch_lines(["*ERROR collecting*"]) - def test_collectonly_display_test_description(self, testdir): + def test_collectonly_displays_test_description( + self, testdir: Testdir, dummy_yaml_custom_test + ) -> None: + """Used dummy_yaml_custom_test for an Item without ``obj``.""" testdir.makepyfile( """ def test_with_description(): - \""" This test has a description. - \""" - assert True - """ + ''' This test has a description. + + more1. + more2.''' + """ ) result = testdir.runpytest("--collect-only", "--verbose") - result.stdout.fnmatch_lines([" This test has a description."]) + result.stdout.fnmatch_lines( + [ + "", + " ", + "", + " ", + " This test has a description.", + " ", + " more1.", + " more2.", + ], + consecutive=True, + ) def test_collectonly_failed_module(self, testdir): testdir.makepyfile("""raise ValueError(0)""") From 1eb2b45db578306a201cf1c51f6fca2b7e407a70 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 31 Mar 2020 07:39:54 +0200 Subject: [PATCH 094/823] Revert "tmpdir: clean up indirection via config for factories (#6767)" This reverts commit 8a1633c3b4a75caff946c1fb5fefaa73f61e5556. + add changelog --- changelog/6992.bugfix.rst | 1 + src/_pytest/tmpdir.py | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 changelog/6992.bugfix.rst diff --git a/changelog/6992.bugfix.rst b/changelog/6992.bugfix.rst new file mode 100644 index 00000000000..2c9b0f89eea --- /dev/null +++ b/changelog/6992.bugfix.rst @@ -0,0 +1 @@ +Revert "tmpdir: clean up indirection via config for factories" #6767 as it breaks pytest-xdist. diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index c1e12da4f0d..85c5b838101 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -14,6 +14,7 @@ from .pathlib import make_numbered_dir_with_cleanup from .pathlib import Path from _pytest.fixtures import FixtureRequest +from _pytest.monkeypatch import MonkeyPatch @attr.s @@ -134,18 +135,35 @@ def get_user() -> Optional[str]: return None +def pytest_configure(config) -> None: + """Create a TempdirFactory and attach it to the config object. + + This is to comply with existing plugins which expect the handler to be + available at pytest_configure time, but ideally should be moved entirely + to the tmpdir_factory session fixture. + """ + mp = MonkeyPatch() + tmppath_handler = TempPathFactory.from_config(config) + t = TempdirFactory(tmppath_handler) + config._cleanup.append(mp.undo) + mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False) + mp.setattr(config, "_tmpdirhandler", t, raising=False) + + @pytest.fixture(scope="session") -def tmpdir_factory(tmp_path_factory) -> TempdirFactory: +def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session. """ - return TempdirFactory(tmp_path_factory) + # Set dynamically by pytest_configure() above. + return request.config._tmpdirhandler # type: ignore @pytest.fixture(scope="session") def tmp_path_factory(request: FixtureRequest) -> TempPathFactory: """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session. """ - return TempPathFactory.from_config(request.config) + # Set dynamically by pytest_configure() above. + return request.config._tmp_path_factory # type: ignore def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: From b553ce54c846e0b9b32ef654d0b68ca55840b87a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Apr 2020 14:46:15 +0300 Subject: [PATCH 095/823] Remove note saying faulthandler_timeout is not available on Windows I think it is available in all Python versions we support, but at least since Python 3.7 the docs[0] say: Changed in version 3.7: This function is now always available. so let's just remove the note. [0] https://docs.python.org/3/library/faulthandler.html#faulthandler.dump_traceback_later --- src/_pytest/faulthandler.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 4ca903146a6..32e3e50c9fe 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -13,8 +13,7 @@ def pytest_addoption(parser): help = ( "Dump the traceback of all threads if a test takes " - "more than TIMEOUT seconds to finish.\n" - "Not available on Windows." + "more than TIMEOUT seconds to finish." ) parser.addini("faulthandler_timeout", help, default=0.0) From 4b634ac758b2b9aff2510ebcf3b6ff773c4853c5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Apr 2020 10:47:25 -0300 Subject: [PATCH 096/823] Remove myself from tidelift --- TIDELIFT.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/TIDELIFT.rst b/TIDELIFT.rst index 062cf6b2504..13aa08cfa13 100644 --- a/TIDELIFT.rst +++ b/TIDELIFT.rst @@ -25,7 +25,6 @@ The current list of contributors receiving funding are: * `@asottile`_ * `@blueyed`_ -* `@nicoddemus`_ Contributors interested in receiving a part of the funds just need to submit a PR adding their name to the list. Contributors that want to stop receiving the funds should also submit a PR From c8fc4c5edcd7ac92226d231c93b2983456dd8640 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Apr 2020 15:38:45 -0700 Subject: [PATCH 097/823] Remove asottile from TIDELIFT.rst --- TIDELIFT.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/TIDELIFT.rst b/TIDELIFT.rst index 13aa08cfa13..e7a19a9ab3b 100644 --- a/TIDELIFT.rst +++ b/TIDELIFT.rst @@ -23,7 +23,6 @@ members of the `contributors team`_ interested in receiving funding. The current list of contributors receiving funding are: -* `@asottile`_ * `@blueyed`_ Contributors interested in receiving a part of the funds just need to submit a PR adding their @@ -54,6 +53,5 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the .. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members .. _`agreement`: https://tidelift.com/docs/lifting/agreement -.. _`@asottile`: https://github.com/asottile .. _`@blueyed`: https://github.com/blueyed .. _`@nicoddemus`: https://github.com/nicoddemus From 5703fdbbdc5de58b769c1748defba9501004c0e5 Mon Sep 17 00:00:00 2001 From: gaurav dhameeja Date: Tue, 17 Mar 2020 21:57:42 +0530 Subject: [PATCH 098/823] Fix-6911(pytest-bot): Added error from commands that are run Earlier pytest-bot would only print out the exception in cases of failure but did not provide context on failing command and error from command. This patch adds the errors from the command to the exception message. `Command` provides abstraction over the command to run and helps in collecting errors from the first failing command only. With this, we don't need to check `returncode` from each command that we run, we can run all the commands and will have access to the error from the first command that failed. This pattern was taken from Go. Please refer: https://blog.golang.org/errors-are-values --- scripts/release-on-comment.py | 59 +++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/scripts/release-on-comment.py b/scripts/release-on-comment.py index 90235fd55c4..5ed71f23675 100644 --- a/scripts/release-on-comment.py +++ b/scripts/release-on-comment.py @@ -33,6 +33,9 @@ from pathlib import Path from subprocess import check_call from subprocess import check_output +from subprocess import PIPE +from subprocess import run +from subprocess import STDOUT from textwrap import dedent from typing import Dict from typing import Optional @@ -91,6 +94,7 @@ def print_and_exit(msg) -> None: def trigger_release(payload_path: Path, token: str) -> None: + error_contents = "" # to be used to store error output in case any command fails payload, base_branch = validate_and_get_issue_comment_payload(payload_path) if base_branch is None: url = get_comment_data(payload)["html_url"] @@ -119,19 +123,42 @@ def trigger_release(payload_path: Path, token: str) -> None: release_branch = f"release-{version}" - check_call(["git", "config", "user.name", "pytest bot"]) - check_call(["git", "config", "user.email", "pytestbot@gmail.com"]) + run( + ["git", "config", "user.name", "pytest bot"], + text=True, + check=True, + capture_output=True, + ) + run( + ["git", "config", "user.email", "pytestbot@gmail.com"], + text=True, + check=True, + capture_output=True, + ) - check_call(["git", "checkout", "-b", release_branch, f"origin/{base_branch}"]) + run( + ["git", "checkout", "-b", release_branch, f"origin/{base_branch}"], + text=True, + check=True, + capture_output=True, + ) print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.") - check_call( - [sys.executable, "scripts/release.py", version, "--skip-check-links"] + run( + [sys.executable, "scripts/release.py", version, "--skip-check-links"], + text=True, + check=True, + capture_output=True, ) oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git" - check_call(["git", "push", oauth_url, f"HEAD:{release_branch}", "--force"]) + run( + ["git", "push", oauth_url, f"HEAD:{release_branch}", "--force"], + text=True, + check=True, + capture_output=True, + ) print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} pushed.") body = PR_BODY.format( @@ -151,7 +178,10 @@ def trigger_release(payload_path: Path, token: str) -> None: print(f"Notified in original comment {Fore.CYAN}{comment.url}{Fore.RESET}.") print(f"{Fore.GREEN}Success.") + except CallProcessError as e: + error_contents = e.output except Exception as e: + error_contents = str(e) link = f"https://github.com/{SLUG}/actions/runs/{os.environ['GITHUB_RUN_ID']}" issue.create_comment( dedent( @@ -168,6 +198,23 @@ def trigger_release(payload_path: Path, token: str) -> None: ) print_and_exit(f"{Fore.RED}{e}") + if error_contents: + link = f"https://github.com/{SLUG}/actions/runs/{os.environ['GITHUB_RUN_ID']}" + issue.create_comment( + dedent( + f""" + Sorry, the request to prepare release `{version}` from {base_branch} failed with: + + ``` + {error_contents} + ``` + + See: {link}. + """ + ) + ) + print_and_exit(f"{Fore.RED}{e}") + def find_next_version(base_branch: str) -> str: output = check_output(["git", "tag"], encoding="UTF-8") From 869c0898876237e2514dcdcc5e63af3ac8c600da Mon Sep 17 00:00:00 2001 From: Andreas Maier Date: Sun, 12 Apr 2020 09:25:54 +0200 Subject: [PATCH 099/823] Fixes #7077: Added & improved docs for repr_failure() in Node & Collector --- src/_pytest/nodes.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 45f0aa8a1de..fa26eb6d41b 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -364,6 +364,14 @@ def _repr_failure_py( def repr_failure( self, excinfo, style=None ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: + """ + Return a string representation of a collection failure. + + :param excinfo: Exception information for the failure as a tuple + (type, value, traceback), see sys.exc_info(). + :param style: Style of the representation ('long', 'short', 'auto', + None). + """ return self._repr_failure_py(excinfo, style) @@ -403,7 +411,12 @@ def collect(self): raise NotImplementedError("abstract") def repr_failure(self, excinfo): - """ represent a collection failure. """ + """ + Return a string representation of a collection failure. + + :param excinfo: Exception information for the failure as a tuple + (type, value, traceback), see sys.exc_info(). + """ if excinfo.errisinstance(self.CollectError) and not self.config.getoption( "fulltrace", False ): From b2582b0314d3c8570af089ec5dc9b3a4a221a870 Mon Sep 17 00:00:00 2001 From: Andreas Maier Date: Sun, 12 Apr 2020 12:00:08 +0200 Subject: [PATCH 100/823] Squash: Applied review comments --- src/_pytest/nodes.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index fa26eb6d41b..0b6e5ec9094 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -362,15 +362,12 @@ def _repr_failure_py( ) def repr_failure( - self, excinfo, style=None + self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]], style=None ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: """ - Return a string representation of a collection failure. + Return a representation of a collection or test failure. - :param excinfo: Exception information for the failure as a tuple - (type, value, traceback), see sys.exc_info(). - :param style: Style of the representation ('long', 'short', 'auto', - None). + :param excinfo: Exception information for the failure. """ return self._repr_failure_py(excinfo, style) @@ -410,12 +407,13 @@ def collect(self): """ raise NotImplementedError("abstract") - def repr_failure(self, excinfo): + def repr_failure( + self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]] + ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: """ - Return a string representation of a collection failure. + Return a representation of a collection or test failure. - :param excinfo: Exception information for the failure as a tuple - (type, value, traceback), see sys.exc_info(). + :param excinfo: Exception information for the failure. """ if excinfo.errisinstance(self.CollectError) and not self.config.getoption( "fulltrace", False From c9386ada29042dec120370c10437c4369f820554 Mon Sep 17 00:00:00 2001 From: Andreas Maier Date: Sun, 12 Apr 2020 13:12:33 +0200 Subject: [PATCH 101/823] Squash: Resolved 2nd round of review comments --- src/_pytest/nodes.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 0b6e5ec9094..6761aa79c52 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -362,7 +362,7 @@ def _repr_failure_py( ) def repr_failure( - self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]], style=None + self, excinfo, style=None ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: """ Return a representation of a collection or test failure. @@ -407,11 +407,9 @@ def collect(self): """ raise NotImplementedError("abstract") - def repr_failure( - self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]] - ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: + def repr_failure(self, excinfo): """ - Return a representation of a collection or test failure. + Return a representation of a collection failure. :param excinfo: Exception information for the failure. """ From 87edc09deaf56dc058631e9491596c54b1ce020b Mon Sep 17 00:00:00 2001 From: symonk Date: Mon, 13 Apr 2020 13:25:06 +0100 Subject: [PATCH 102/823] Gracefully handle eval() failure(s) for marker expressions --- AUTHORS | 1 + changelog/4583.bugfix.rst | 1 + src/_pytest/mark/legacy.py | 9 ++++++-- testing/test_mark.py | 47 +++++++++++++++++++++++++++++++++++--- 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 changelog/4583.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 6efc41d9712..bbb8e463dc9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -246,6 +246,7 @@ Segev Finer Serhii Mozghovyi Seth Junot Simon Gomizelj +Simon Kerr Skylar Downes Srinivas Reddy Thatiparthy Stefan Farmbauer diff --git a/changelog/4583.bugfix.rst b/changelog/4583.bugfix.rst new file mode 100644 index 00000000000..64a680f1429 --- /dev/null +++ b/changelog/4583.bugfix.rst @@ -0,0 +1 @@ +Prevent crashing and provide user friendly error(s) when marker expressions (-m) invoking of eval() raises a SyntaxError or TypeError diff --git a/src/_pytest/mark/legacy.py b/src/_pytest/mark/legacy.py index 80a520a0a9e..7581421daf1 100644 --- a/src/_pytest/mark/legacy.py +++ b/src/_pytest/mark/legacy.py @@ -84,8 +84,13 @@ def matchmark(colitem, markexpr): """Tries to match on any marker names, attached to the given colitem.""" try: return eval(markexpr, {}, MarkMapping.from_item(colitem)) - except SyntaxError as e: - raise SyntaxError(str(e) + "\nMarker expression must be valid Python!") + except (SyntaxError, TypeError): + raise UsageError( + "Marker expression provided to -m:{} was not valid python syntax." + " Please check the syntax provided and ensure it is correct".format( + markexpr + ) + ) def matchkeyword(colitem, keywordexpr): diff --git a/testing/test_mark.py b/testing/test_mark.py index 530f9f1688c..69f751012cb 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -608,10 +608,13 @@ def test_a(): pass """ ) - result = testdir.runpytest("-m bogus/") - result.stdout.fnmatch_lines( - ["INTERNALERROR> Marker expression must be valid Python!"] + expected = ( + "ERROR: Marker expression provided to -m: bogus/ was not valid python syntax. Please " + "check the syntax provided and ensure it is correct" ) + result = testdir.runpytest("-m bogus/") + result.stderr.fnmatch_lines([expected]) + assert result.ret == ExitCode.USAGE_ERROR def test_keywords_at_node_level(self, testdir): testdir.makepyfile( @@ -1022,3 +1025,41 @@ def test_pytest_param_id_requires_string(): @pytest.mark.parametrize("s", (None, "hello world")) def test_pytest_param_id_allows_none_or_string(s): assert pytest.param(id=s) + + +def test_ux_eval_syntax_error(testdir): + foo = testdir.makepyfile( + """ + import pytest + + @pytest.mark.internal_err + def test_foo(): + pass + """ + ) + expected = ( + "ERROR: Marker expression provided to -m: NOT internal_err was not valid python syntax. Please " + "check the syntax provided and ensure it is correct" + ) + result = testdir.runpytest(foo, "-m NOT internal_err") + result.stderr.fnmatch_lines([expected]) + assert result.ret == ExitCode.USAGE_ERROR + + +def test_ux_eval_type_error(testdir): + foo = testdir.makepyfile( + """ + import pytest + + @pytest.mark.internal_err + def test_foo(): + pass + """ + ) + expected = ( + "ERROR: Marker expression provided to -m: NOT (internal_err) was not valid python syntax. Please " + "check the syntax provided and ensure it is correct" + ) + result = testdir.runpytest(foo, "-m NOT (internal_err)") + result.stderr.fnmatch_lines([expected]) + assert result.ret == ExitCode.USAGE_ERROR From 251e8f212e49a94b999c057fa5164a11faa7eed2 Mon Sep 17 00:00:00 2001 From: symonk Date: Mon, 13 Apr 2020 14:25:01 +0100 Subject: [PATCH 103/823] refactor mark tests, widen catching and make error msg more concise --- src/_pytest/mark/legacy.py | 9 ++------ testing/test_mark.py | 44 ++++---------------------------------- 2 files changed, 6 insertions(+), 47 deletions(-) diff --git a/src/_pytest/mark/legacy.py b/src/_pytest/mark/legacy.py index 7581421daf1..eb50340f249 100644 --- a/src/_pytest/mark/legacy.py +++ b/src/_pytest/mark/legacy.py @@ -84,13 +84,8 @@ def matchmark(colitem, markexpr): """Tries to match on any marker names, attached to the given colitem.""" try: return eval(markexpr, {}, MarkMapping.from_item(colitem)) - except (SyntaxError, TypeError): - raise UsageError( - "Marker expression provided to -m:{} was not valid python syntax." - " Please check the syntax provided and ensure it is correct".format( - markexpr - ) - ) + except Exception: + raise UsageError("Wrong expression passed to '-m': {}".format(markexpr)) def matchkeyword(colitem, keywordexpr): diff --git a/testing/test_mark.py b/testing/test_mark.py index 69f751012cb..2aad2b1ba5a 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -601,21 +601,6 @@ def test_unmarked(): deselected_tests = dlist[0].items assert len(deselected_tests) == 2 - def test_invalid_m_option(self, testdir): - testdir.makepyfile( - """ - def test_a(): - pass - """ - ) - expected = ( - "ERROR: Marker expression provided to -m: bogus/ was not valid python syntax. Please " - "check the syntax provided and ensure it is correct" - ) - result = testdir.runpytest("-m bogus/") - result.stderr.fnmatch_lines([expected]) - assert result.ret == ExitCode.USAGE_ERROR - def test_keywords_at_node_level(self, testdir): testdir.makepyfile( """ @@ -1027,26 +1012,8 @@ def test_pytest_param_id_allows_none_or_string(s): assert pytest.param(id=s) -def test_ux_eval_syntax_error(testdir): - foo = testdir.makepyfile( - """ - import pytest - - @pytest.mark.internal_err - def test_foo(): - pass - """ - ) - expected = ( - "ERROR: Marker expression provided to -m: NOT internal_err was not valid python syntax. Please " - "check the syntax provided and ensure it is correct" - ) - result = testdir.runpytest(foo, "-m NOT internal_err") - result.stderr.fnmatch_lines([expected]) - assert result.ret == ExitCode.USAGE_ERROR - - -def test_ux_eval_type_error(testdir): +@pytest.mark.parametrize("expr", ("NOT internal_err", "NOT (internal_err)", "bogus/")) +def test_marker_expr_eval_failure_handling(testdir, expr): foo = testdir.makepyfile( """ import pytest @@ -1056,10 +1023,7 @@ def test_foo(): pass """ ) - expected = ( - "ERROR: Marker expression provided to -m: NOT (internal_err) was not valid python syntax. Please " - "check the syntax provided and ensure it is correct" - ) - result = testdir.runpytest(foo, "-m NOT (internal_err)") + expected = "ERROR: Wrong expression passed to '-m': {}".format(expr) + result = testdir.runpytest(foo, "-m", expr) result.stderr.fnmatch_lines([expected]) assert result.ret == ExitCode.USAGE_ERROR From 6fd30134d323919ed38c8c9fe0785f7d9e0779ca Mon Sep 17 00:00:00 2001 From: Simon K Date: Mon, 13 Apr 2020 14:29:59 +0100 Subject: [PATCH 104/823] Update changelog/4583.bugfix.rst Co-Authored-By: Ran Benita --- changelog/4583.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/4583.bugfix.rst b/changelog/4583.bugfix.rst index 64a680f1429..f0a82030338 100644 --- a/changelog/4583.bugfix.rst +++ b/changelog/4583.bugfix.rst @@ -1 +1 @@ -Prevent crashing and provide user friendly error(s) when marker expressions (-m) invoking of eval() raises a SyntaxError or TypeError +Prevent crashing and provide a user-friendly error when a marker expression (-m) invoking of eval() raises any exception. From f479cbce10baa86234874b5e6b607c0b48913c78 Mon Sep 17 00:00:00 2001 From: Simon K Date: Mon, 13 Apr 2020 18:58:50 +0100 Subject: [PATCH 105/823] Document pytester fixtures so --fixtures makes more sense (#7080) --- src/_pytest/pytester.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index f25f8e10c5d..5fe8c21ff00 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -357,16 +357,33 @@ def clear(self) -> None: @pytest.fixture def linecomp() -> "LineComp": + """ + A :class: `LineComp` instance for checking that an input linearly + contains a sequence of strings. + """ return LineComp() @pytest.fixture(name="LineMatcher") def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]": + """ + A reference to the :class: `LineMatcher`. + + This is instantiable with a list of lines (without their trailing newlines). + This is useful for testing large texts, such as the output of commands. + """ return LineMatcher @pytest.fixture def testdir(request: FixtureRequest, tmpdir_factory) -> "Testdir": + """ + A :class: `TestDir` instance, that can be used to run and test pytest itself. + + It is particularly useful for testing plugins. It is similar to the `tmpdir` fixture + but provides methods which aid in testing pytest itself. + + """ return Testdir(request, tmpdir_factory) From 10080dc60dc22b51bfcaea80ce2e68a922be9d9a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 14 Apr 2020 14:49:12 +0300 Subject: [PATCH 106/823] Remove pypy (2) environment from tox.ini pypy refers to Pypy 2 which implements Python 2 which pytest does not support. Keep only pypy3. --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index ee3466bac02..3a280abb782 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ envlist = py36 py37 py38 - pypy pypy3 py37-{pexpect,xdist,twisted,numpy,pluggymaster} doctesting From de6c28ed1f26f3ffa937472de2967e03c1da044a Mon Sep 17 00:00:00 2001 From: Simon K Date: Wed, 15 Apr 2020 10:17:13 +0100 Subject: [PATCH 107/823] Improve error handling around yieldctx fixtures which do not yield a value (#7083) --- changelog/7061.bugfix.rst | 1 + src/_pytest/fixtures.py | 15 ++++++++++----- testing/python/fixtures.py | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 changelog/7061.bugfix.rst diff --git a/changelog/7061.bugfix.rst b/changelog/7061.bugfix.rst new file mode 100644 index 00000000000..7e6d36c2fb1 --- /dev/null +++ b/changelog/7061.bugfix.rst @@ -0,0 +1 @@ +When a yielding fixture fails to yield a value, report a test setup error instead of crashing. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f9e0f3b28b5..f98be82780a 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -786,13 +786,18 @@ def fail_fixturefunc(fixturefunc, msg): def call_fixture_func(fixturefunc, request, kwargs): yieldctx = is_generator(fixturefunc) if yieldctx: - it = fixturefunc(**kwargs) - res = next(it) - finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, it) + generator = fixturefunc(**kwargs) + try: + fixture_result = next(generator) + except StopIteration: + raise ValueError( + "{} did not yield a value".format(request.fixturename) + ) from None + finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator) request.addfinalizer(finalizer) else: - res = fixturefunc(**kwargs) - return res + fixture_result = fixturefunc(**kwargs) + return fixture_result def _teardown_yield_fixture(fixturefunc, it): diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 36e55a0e1de..8cce3ebde1d 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3,6 +3,7 @@ import pytest from _pytest import fixtures +from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest from _pytest.pathlib import Path from _pytest.pytester import get_public_names @@ -4290,3 +4291,23 @@ def test_suffix(fix_combined): ) result = testdir.runpytest("-vv", str(p1)) assert result.ret == 0 + + +def test_yield_fixture_with_no_value(testdir): + testdir.makepyfile( + """ + import pytest + @pytest.fixture(name='custom') + def empty_yield(): + if False: + yield + + def test_fixt(custom): + pass + """ + ) + expected = "E ValueError: custom did not yield a value" + result = testdir.runpytest() + result.assert_outcomes(error=1) + result.stdout.fnmatch_lines([expected]) + assert result.ret == ExitCode.TESTS_FAILED From 7789b51acb3cb98f2bdd59fb0cd926376d65f1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Katarzyna=20Kr=C3=B3l?= <38542683+CarycaKatarzyna@users.noreply.github.com> Date: Fri, 17 Apr 2020 07:28:36 +0200 Subject: [PATCH 108/823] Issue 4677 - always relative path in skip report (#6953) --- AUTHORS | 1 + changelog/4677.bugfix.rst | 1 + src/_pytest/terminal.py | 14 +++++++++----- testing/test_skipping.py | 2 +- testing/test_terminal.py | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 changelog/4677.bugfix.rst diff --git a/AUTHORS b/AUTHORS index bbb8e463dc9..cfbcf432b6c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -148,6 +148,7 @@ Justyna Janczyszyn Kale Kundert Karl O. Pinc Katarzyna Jachim +Katarzyna Król Katerina Koukiou Kevin Cox Kevin J. Foley diff --git a/changelog/4677.bugfix.rst b/changelog/4677.bugfix.rst new file mode 100644 index 00000000000..6b7d2cf179d --- /dev/null +++ b/changelog/4677.bugfix.rst @@ -0,0 +1 @@ +The path shown in the summary report for SKIPPED tests is now always relative. Previously it was sometimes absolute. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index a99463fe875..52c04a49c3b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1027,7 +1027,7 @@ def show_xpassed(lines: List[str]) -> None: def show_skipped(lines: List[str]) -> None: skipped = self.stats.get("skipped", []) - fskips = _folded_skips(skipped) if skipped else [] + fskips = _folded_skips(self.startdir, skipped) if skipped else [] if not fskips: return verbose_word = skipped[0]._get_verbose_word(self.config) @@ -1153,11 +1153,13 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): return line -def _folded_skips(skipped): +def _folded_skips(startdir, skipped): d = {} for event in skipped: - key = event.longrepr - assert len(key) == 3, (event, key) + assert len(event.longrepr) == 3, (event, event.longrepr) + fspath, lineno, reason = event.longrepr + # For consistency, report all fspaths in relative form. + fspath = startdir.bestrelpath(py.path.local(fspath)) keywords = getattr(event, "keywords", {}) # folding reports with global pytestmark variable # this is workaround, because for now we cannot identify the scope of a skip marker @@ -1167,7 +1169,9 @@ def _folded_skips(skipped): and "skip" in keywords and "pytestmark" not in keywords ): - key = (key[0], None, key[2]) + key = (fspath, None, reason) + else: + key = (fspath, lineno, reason) d.setdefault(key, []).append(event) values = [] for key, events in d.items(): diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 67714d030ed..8b1cdd527ad 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -758,7 +758,7 @@ def doskip(): result = testdir.runpytest("-rs") result.stdout.fnmatch_lines_random( [ - "SKIPPED [[]2[]] */conftest.py:4: test", + "SKIPPED [[]2[]] conftest.py:4: test", "SKIPPED [[]1[]] test_one.py:14: via_decorator", ] ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 88d564519e0..d2857ca775f 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2001,7 +2001,7 @@ class X: ev3.longrepr = longrepr ev3.skipped = True - values = _folded_skips([ev1, ev2, ev3]) + values = _folded_skips(py.path.local(), [ev1, ev2, ev3]) assert len(values) == 1 num, fspath, lineno, reason = values[0] assert num == 3 From 907e29a47b666067af4e64eb694d71cf72f61591 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 17 Apr 2020 16:05:43 +0300 Subject: [PATCH 109/823] fixtures: deprecate pytest._fillfuncargs function This function is exposed and kept alive for the oejskit plugin which is abandoned and no longer works with recent plugins, so let's prepare to completely remove it. --- changelog/7097.deprecation.rst | 6 ++++++ doc/en/deprecations.rst | 13 +++++++++++++ src/_pytest/deprecated.py | 5 +++++ src/_pytest/fixtures.py | 2 ++ src/_pytest/python.py | 2 +- testing/deprecated_test.py | 9 +++++++++ testing/python/fixtures.py | 6 +++--- testing/python/integration.py | 4 ++-- 8 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 changelog/7097.deprecation.rst diff --git a/changelog/7097.deprecation.rst b/changelog/7097.deprecation.rst new file mode 100644 index 00000000000..ed9779e1f50 --- /dev/null +++ b/changelog/7097.deprecation.rst @@ -0,0 +1,6 @@ +The ``pytest._fillfuncargs`` function is now deprecated. This function was kept +for backward compatibility with an older plugin. + +It's functionality is not meant to be used directly, but if you must replace +it, use `function._request._fillfixtures()` instead, though note this is not +a public API and may break in the future. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 13d59bce2b7..4d8177a543d 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -20,6 +20,19 @@ Below is a complete list of all pytest features which are considered deprecated. :ref:`standard warning filters `. +The ``pytest._fillfuncargs`` function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.5 + +This function was kept for backward compatibility with an older plugin. + +It's functionality is not meant to be used directly, but if you must replace +it, use `function._request._fillfixtures()` instead, though note this is not +a public API and may break in the future. + + + ``--no-print-logs`` command-line option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 926cfcf19dc..2295bfe1d28 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -25,6 +25,11 @@ "since pytest 2.3 - use the newer attribute instead." ) +FILLFUNCARGS = PytestDeprecationWarning( + "The `_fillfuncargs` function is deprecated, use " + "function._request._fillfixtures() instead if you must." +) + RESULT_LOG = PytestDeprecationWarning( "--result-log is deprecated, please try the new pytest-reportlog plugin.\n" "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f98be82780a..f673885c731 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -28,6 +28,7 @@ from _pytest.compat import NOTSET from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING +from _pytest.deprecated import FILLFUNCARGS from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS from _pytest.deprecated import FUNCARGNAMES from _pytest.mark import ParameterSet @@ -276,6 +277,7 @@ def reorder_items_atscope(items, argkeys_cache, items_by_argkey, scopenum): def fillfixtures(function): """ fill missing funcargs for a test function. """ + warnings.warn(FILLFUNCARGS, stacklevel=2) try: request = function._request except AttributeError: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 1f6a095c4e1..2b9bf4f5bb5 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1535,7 +1535,7 @@ def setup(self) -> None: if isinstance(self.parent, Instance): self.parent.newinstance() self.obj = self._getobj() - fixtures.fillfixtures(self) + self._request._fillfixtures() def _prunetraceback(self, excinfo: ExceptionInfo) -> None: if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index cf7dee854aa..caa44854f5f 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,4 +1,5 @@ import inspect +from unittest import mock import pytest from _pytest import deprecated @@ -146,3 +147,11 @@ def test_foo(): ) assert_no_print_logs(testdir, ()) + + +def test__fillfuncargs_is_deprecated() -> None: + with pytest.warns( + pytest.PytestDeprecationWarning, + match="The `_fillfuncargs` function is deprecated", + ): + pytest._fillfuncargs(mock.Mock()) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8cce3ebde1d..226a7987408 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -110,7 +110,7 @@ def test_detect_recursive_dependency_error(self, testdir): def test_funcarg_basic(self, testdir): testdir.copy_example() item = testdir.getitem(Path("test_funcarg_basic.py")) - fixtures.fillfixtures(item) + item._request._fillfixtures() del item.funcargs["request"] assert len(get_public_names(item.funcargs)) == 2 assert item.funcargs["some"] == "test_func" @@ -664,7 +664,7 @@ def test_func(something): pass assert val2 == 2 val2 = req.getfixturevalue("other") # see about caching assert val2 == 2 - pytest._fillfuncargs(item) + item._request._fillfixtures() assert item.funcargs["something"] == 1 assert len(get_public_names(item.funcargs)) == 2 assert "request" in item.funcargs @@ -681,7 +681,7 @@ def test_func(something): pass """ ) item.session._setupstate.prepare(item) - pytest._fillfuncargs(item) + item._request._fillfixtures() # successively check finalization calls teardownlist = item.getparent(pytest.Module).obj.teardownlist ss = item.session._setupstate diff --git a/testing/python/integration.py b/testing/python/integration.py index 35e86e6b96c..3409b644647 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -4,7 +4,7 @@ class TestOEJSKITSpecials: - def test_funcarg_non_pycollectobj(self, testdir): # rough jstests usage + def test_funcarg_non_pycollectobj(self, testdir, recwarn): # rough jstests usage testdir.makeconftest( """ import pytest @@ -34,7 +34,7 @@ class MyClass(object): pytest._fillfuncargs(clscol) assert clscol.funcargs["arg1"] == 42 - def test_autouse_fixture(self, testdir): # rough jstests usage + def test_autouse_fixture(self, testdir, recwarn): # rough jstests usage testdir.makeconftest( """ import pytest From e269407e650b58b50abb5f97f3191869fa24e303 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 24 Apr 2020 19:02:31 +0300 Subject: [PATCH 110/823] testing: avoid pytest_collect_directory message in warnings summary Currently this test issues a warning which is displayed in the warning summary (of pytest's own test suite): testing/acceptance_test.py::TestGeneralUsage::test_early_skip /tmp/pytest-of-ran/pytest-396/test_early_skip0/conftest.py:2: PytestDeprecationWarning: The pytest_collect_directory hook is not working. Please use collect_ignore in conftests or pytest_collection_modifyitems. def pytest_collect_directory(): I think the filter was meant to be `ignore` in the first place, and not `always` which is not a valid action AFAIK. --- testing/acceptance_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 0ac3be12205..fd330f6700a 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -223,7 +223,7 @@ def foo(): "E {}: No module named 'qwerty'".format(exc_name), ] - @pytest.mark.filterwarnings("always::pytest.PytestDeprecationWarning") + @pytest.mark.filterwarnings("ignore::pytest.PytestDeprecationWarning") def test_early_skip(self, testdir): testdir.mkdir("xyz") testdir.makeconftest( From 289e6c1d3632e5f9c53d4f2986ecc7158ac8abe7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 24 Apr 2020 21:37:08 +0300 Subject: [PATCH 111/823] Update src/_pytest/deprecated.py Co-Authored-By: Ronny Pfannschmidt --- src/_pytest/deprecated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 2295bfe1d28..1896a05405f 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -27,7 +27,7 @@ FILLFUNCARGS = PytestDeprecationWarning( "The `_fillfuncargs` function is deprecated, use " - "function._request._fillfixtures() instead if you must." + "function._request._fillfixtures() instead if you cannot avoid reaching into internals." ) RESULT_LOG = PytestDeprecationWarning( From 38c9d59ddc34363f70d2b7cd3b2e93e58a16d7fa Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 24 Apr 2020 21:50:20 +0300 Subject: [PATCH 112/823] pre-commit: update blacken-docs 1.0.0 -> 1.6.0 --- .pre-commit-config.yaml | 2 +- src/_pytest/mark/__init__.py | 8 ++++---- src/_pytest/monkeypatch.py | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c47b557693..796ba177bb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: black args: [--safe, --quiet] - repo: https://github.com/asottile/blacken-docs - rev: v1.0.0 + rev: v1.6.0 hooks: - id: blacken-docs additional_dependencies: [black==19.10b0] diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index dab0cf149fd..36245c25a05 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -27,10 +27,10 @@ def param(*values, **kw): .. code-block:: python - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - pytest.param("6*9", 42, marks=pytest.mark.xfail), - ]) + @pytest.mark.parametrize( + "test_input,expected", + [("3+5", 8), pytest.param("6*9", 42, marks=pytest.mark.xfail),], + ) def test_eval(test_input, expected): assert eval(test_input) == expected diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index ce1c0f65102..9d802a62578 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -117,6 +117,8 @@ def context(self) -> Generator["MonkeyPatch", None, None]: .. code-block:: python import functools + + def test_partial(monkeypatch): with monkeypatch.context() as m: m.setattr(functools, "partial", 3) From 23881ad592f4c4bb4ecae55c6ac44612a4c7422f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 24 Apr 2020 21:50:44 +0300 Subject: [PATCH 113/823] pre-commit: update pre-commit-hooks 2.2.3 -> 2.5.0 --- .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 796ba177bb4..7caa4439c51 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: blacken-docs additional_dependencies: [black==19.10b0] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.2.3 + rev: v2.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From 3cd97d50f98eac7d646e7773fd48b87974c14941 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 24 Apr 2020 21:51:07 +0300 Subject: [PATCH 114/823] pre-commit: update pyupgrade 1.18.0 -> 2.2.1 --- .pre-commit-config.yaml | 2 +- src/_pytest/capture.py | 6 +++--- src/_pytest/pytester.py | 4 ++-- testing/acceptance_test.py | 2 +- testing/logging/test_reporting.py | 4 ++-- testing/test_capture.py | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7caa4439c51..fd909dd5da5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: - id: reorder-python-imports args: ['--application-directories=.:src', --py3-plus] - repo: https://github.com/asottile/pyupgrade - rev: v1.18.0 + rev: v2.2.1 hooks: - id: pyupgrade args: [--py3-plus] diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 7096f95b298..2798a24b4c4 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -505,7 +505,7 @@ def __init__(self, targetfd, tmpfile=None): self.done = self._done if targetfd == 0: assert not tmpfile, "cannot set tmpfile with stdin" - tmpfile = open(os.devnull, "r") + tmpfile = open(os.devnull) self.syscapture = SysCapture(targetfd) else: if tmpfile is None: @@ -580,7 +580,7 @@ class FDCapture(FDCaptureBinary): """ # Ignore type because it doesn't match the type in the superclass (bytes). - EMPTY_BUFFER = str() # type: ignore + EMPTY_BUFFER = "" # type: ignore def snap(self): self.tmpfile.seek(0) @@ -651,7 +651,7 @@ def writeorg(self, data): class SysCapture(SysCaptureBinary): - EMPTY_BUFFER = str() # type: ignore[assignment] # noqa: F821 + EMPTY_BUFFER = "" # type: ignore[assignment] # noqa: F821 def snap(self): res = self.tmpfile.getvalue() diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 5fe8c21ff00..9fcda8f0bed 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1202,8 +1202,8 @@ def handle_timeout(): finally: f1.close() f2.close() - f1 = open(str(p1), "r", encoding="utf8") - f2 = open(str(p2), "r", encoding="utf8") + f1 = open(str(p1), encoding="utf8") + f2 = open(str(p2), encoding="utf8") try: out = f1.read().splitlines() err = f2.read().splitlines() diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index fd330f6700a..36a24a38a14 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1339,7 +1339,7 @@ def test_simple(): result.stderr.fnmatch_lines(["*@this is stderr@*"]) # now ensure the output is in the junitxml - with open(os.path.join(testdir.tmpdir.strpath, "output.xml"), "r") as f: + with open(os.path.join(testdir.tmpdir.strpath, "output.xml")) as f: fullXml = f.read() assert "@this is stdout@\n" in fullXml assert "@this is stderr@\n" in fullXml diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 4333bbb0028..ad7af937008 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -1103,11 +1103,11 @@ def test_second(): """ ) testdir.runpytest() - with open(os.path.join(report_dir_base, "test_first"), "r") as rfh: + with open(os.path.join(report_dir_base, "test_first")) as rfh: content = rfh.read() assert "message from test 1" in content - with open(os.path.join(report_dir_base, "test_second"), "r") as rfh: + with open(os.path.join(report_dir_base, "test_second")) as rfh: content = rfh.read() assert "message from test 2" in content diff --git a/testing/test_capture.py b/testing/test_capture.py index 132ce1cc645..b059073b5c6 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1401,14 +1401,14 @@ def test_global(fix1): result = testdir.runpytest_subprocess("--log-cli-level=INFO") assert result.ret == 0 - with open("caplog", "r") as f: + with open("caplog") as f: caplog = f.read() assert "fix setup" in caplog assert "something in test" in caplog assert "fix teardown" in caplog - with open("capstdout", "r") as f: + with open("capstdout") as f: capstdout = f.read() assert "fix setup" in capstdout From 49e50d3106ac44aee8f20151330e0df35ae00cca Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 28 Apr 2020 22:46:44 +0300 Subject: [PATCH 115/823] testing: fix warning issued by test_cache_writefail_cachfile_silent Remove this message which was shown in the warning summary of pytest's own testsuite: testing/test_cacheprovider.py::TestNewAPI::test_cache_writefail_cachfile_silent testing/test_cacheprovider.py:40: PytestCacheWarning: could not create cache path /tmp/pytest-of-ran/pytest-2/test_cache_writefail_cachfile_silent0/.pytest_cache/v/test/broken cache.set("test/broken", []) --- testing/test_cacheprovider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index eb381ab500f..571bef4e40e 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -31,7 +31,7 @@ def test_config_cache_dataerror(self, testdir): val = config.cache.get("key/name", -2) assert val == -2 - @pytest.mark.filterwarnings("default") + @pytest.mark.filterwarnings("ignore:could not create cache path") def test_cache_writefail_cachfile_silent(self, testdir): testdir.makeini("[pytest]") testdir.tmpdir.join(".pytest_cache").write("gone wrong") From bef263ee76d8ed3a278621a3243cd03810d08b0d Mon Sep 17 00:00:00 2001 From: ArtyomKaltovich Date: Tue, 28 Apr 2020 23:04:58 +0300 Subject: [PATCH 116/823] update doctests.rst regarding issue #7116 https://github.com/pytest-dev/pytest/issues/7116 --- doc/en/doctest.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index b73cc994ab3..a85ac3d6442 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -8,7 +8,7 @@ can change the pattern by issuing: .. code-block:: bash - pytest --doctest-glob='*.rst' + pytest --doctest-glob="*.rst" on the command line. ``--doctest-glob`` can be given multiple times in the command-line. From 4a42afdc2f3bb21f1685ac5c49b806ae34c36355 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 28 Apr 2020 22:26:54 +0300 Subject: [PATCH 117/823] cacheprovider: speed up NFPlugin when --nf is not enabled The code used an O(n^2) loop. Replace list with set to make it O(n). For backward compatibility the filesystem cache still remains a list. On this test: import pytest @pytest.mark.parametrize("x", range(5000)) def test_foo(x): pass run with `pytest --collect-only`: Before: 0m1.251s After: 0m0.921s --- src/_pytest/cacheprovider.py | 12 +++++------- testing/test_cacheprovider.py | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 2f13067bc00..1333522623e 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -332,13 +332,13 @@ class NFPlugin: def __init__(self, config): self.config = config self.active = config.option.newfirst - self.cached_nodeids = config.cache.get("cache/nodeids", []) + self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) def pytest_collection_modifyitems( self, session: Session, config: Config, items: List[nodes.Item] ) -> None: - new_items = OrderedDict() # type: OrderedDict[str, nodes.Item] if self.active: + new_items = OrderedDict() # type: OrderedDict[str, nodes.Item] other_items = OrderedDict() # type: OrderedDict[str, nodes.Item] for item in items: if item.nodeid not in self.cached_nodeids: @@ -349,11 +349,9 @@ def pytest_collection_modifyitems( items[:] = self._get_increasing_order( new_items.values() ) + self._get_increasing_order(other_items.values()) + self.cached_nodeids.update(new_items) else: - for item in items: - if item.nodeid not in self.cached_nodeids: - new_items[item.nodeid] = item - self.cached_nodeids.extend(new_items) + self.cached_nodeids.update(item.nodeid for item in items) def _get_increasing_order(self, items): return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) @@ -363,7 +361,7 @@ def pytest_sessionfinish(self, session): if config.getoption("cacheshow") or hasattr(config, "slaveinput"): return - config.cache.set("cache/nodeids", self.cached_nodeids) + config.cache.set("cache/nodeids", sorted(self.cached_nodeids)) def pytest_addoption(parser): diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index eb381ab500f..0b1ab61269a 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -74,7 +74,7 @@ def test_cache_failure_warns(self, testdir, monkeypatch): "*/cacheprovider.py:*", " */cacheprovider.py:*: PytestCacheWarning: could not create cache path " "{}/v/cache/nodeids".format(cache_dir), - ' config.cache.set("cache/nodeids", self.cached_nodeids)', + ' config.cache.set("cache/nodeids", sorted(self.cached_nodeids))', "*1 failed, 3 warnings in*", ] ) From c72a1b29330c5fc6027b5438a9e826c522c89bd0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 14:33:57 +0300 Subject: [PATCH 118/823] config: replace usage of py.io.dupfile As part of the effort to remove uses of `py`. --- src/_pytest/config/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6d18eeb6554..8e5944fc701 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -308,7 +308,9 @@ def __init__(self): err = sys.stderr encoding = getattr(err, "encoding", "utf8") try: - err = py.io.dupfile(err, encoding=encoding) + err = open( + os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding, + ) except Exception: pass self.trace.root.setwriter(err.write) From 276405a0394e6ade96b23aba493fef0e6fccdbd5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 14:49:32 +0300 Subject: [PATCH 119/823] terminalwriter: vendor TerminalWriter from py Straight copy from py 1.8.1. Doesn't pass linting yet. --- src/_pytest/_io/__init__.py | 2 +- src/_pytest/_io/terminalwriter.py | 421 ++++++++++++++++++++++++++++++ 2 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 src/_pytest/_io/terminalwriter.py diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index 28ddc7b78ed..d401cda8e13 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -1,7 +1,7 @@ from typing import List from typing import Sequence -from py.io import TerminalWriter as BaseTerminalWriter # noqa: F401 +from .terminalwriter import TerminalWriter as BaseTerminalWriter class TerminalWriter(BaseTerminalWriter): diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py new file mode 100644 index 00000000000..be559867c22 --- /dev/null +++ b/src/_pytest/_io/terminalwriter.py @@ -0,0 +1,421 @@ +""" + +Helper functions for writing to terminals and files. + +""" + + +import sys, os, unicodedata +import py +py3k = sys.version_info[0] >= 3 +py33 = sys.version_info >= (3, 3) +from py.builtin import text, bytes + +win32_and_ctypes = False +colorama = None +if sys.platform == "win32": + try: + import colorama + except ImportError: + try: + import ctypes + win32_and_ctypes = True + except ImportError: + pass + + +def _getdimensions(): + if py33: + import shutil + size = shutil.get_terminal_size() + return size.lines, size.columns + else: + import termios, fcntl, struct + call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8) + height, width = struct.unpack("hhhh", call)[:2] + return height, width + + +def get_terminal_width(): + width = 0 + try: + _, width = _getdimensions() + except py.builtin._sysex: + raise + except: + # pass to fallback below + pass + + if width == 0: + # FALLBACK: + # * some exception happened + # * or this is emacs terminal which reports (0,0) + width = int(os.environ.get('COLUMNS', 80)) + + # XXX the windows getdimensions may be bogus, let's sanify a bit + if width < 40: + width = 80 + return width + +terminal_width = get_terminal_width() + +char_width = { + 'A': 1, # "Ambiguous" + 'F': 2, # Fullwidth + 'H': 1, # Halfwidth + 'N': 1, # Neutral + 'Na': 1, # Narrow + 'W': 2, # Wide +} + + +def get_line_width(text): + text = unicodedata.normalize('NFC', text) + return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text) + + +# XXX unify with _escaped func below +def ansi_print(text, esc, file=None, newline=True, flush=False): + if file is None: + file = sys.stderr + text = text.rstrip() + if esc and not isinstance(esc, tuple): + esc = (esc,) + if esc and sys.platform != "win32" and file.isatty(): + text = (''.join(['\x1b[%sm' % cod for cod in esc]) + + text + + '\x1b[0m') # ANSI color code "reset" + if newline: + text += '\n' + + if esc and win32_and_ctypes and file.isatty(): + if 1 in esc: + bold = True + esc = tuple([x for x in esc if x != 1]) + else: + bold = False + esctable = {() : FOREGROUND_WHITE, # normal + (31,): FOREGROUND_RED, # red + (32,): FOREGROUND_GREEN, # green + (33,): FOREGROUND_GREEN|FOREGROUND_RED, # yellow + (34,): FOREGROUND_BLUE, # blue + (35,): FOREGROUND_BLUE|FOREGROUND_RED, # purple + (36,): FOREGROUND_BLUE|FOREGROUND_GREEN, # cyan + (37,): FOREGROUND_WHITE, # white + (39,): FOREGROUND_WHITE, # reset + } + attr = esctable.get(esc, FOREGROUND_WHITE) + if bold: + attr |= FOREGROUND_INTENSITY + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 + if file is sys.stderr: + handle = GetStdHandle(STD_ERROR_HANDLE) + else: + handle = GetStdHandle(STD_OUTPUT_HANDLE) + oldcolors = GetConsoleInfo(handle).wAttributes + attr |= (oldcolors & 0x0f0) + SetConsoleTextAttribute(handle, attr) + while len(text) > 32768: + file.write(text[:32768]) + text = text[32768:] + if text: + file.write(text) + SetConsoleTextAttribute(handle, oldcolors) + else: + file.write(text) + + if flush: + file.flush() + +def should_do_markup(file): + if os.environ.get('PY_COLORS') == '1': + return True + if os.environ.get('PY_COLORS') == '0': + return False + return hasattr(file, 'isatty') and file.isatty() \ + and os.environ.get('TERM') != 'dumb' \ + and not (sys.platform.startswith('java') and os._name == 'nt') + +class TerminalWriter(object): + _esctable = dict(black=30, red=31, green=32, yellow=33, + blue=34, purple=35, cyan=36, white=37, + Black=40, Red=41, Green=42, Yellow=43, + Blue=44, Purple=45, Cyan=46, White=47, + bold=1, light=2, blink=5, invert=7) + + # XXX deprecate stringio argument + def __init__(self, file=None, stringio=False, encoding=None): + if file is None: + if stringio: + self.stringio = file = py.io.TextIO() + else: + from sys import stdout as file + elif py.builtin.callable(file) and not ( + hasattr(file, "write") and hasattr(file, "flush")): + file = WriteFile(file, encoding=encoding) + if hasattr(file, "isatty") and file.isatty() and colorama: + file = colorama.AnsiToWin32(file).stream + self.encoding = encoding or getattr(file, 'encoding', "utf-8") + self._file = file + self.hasmarkup = should_do_markup(file) + self._lastlen = 0 + self._chars_on_current_line = 0 + self._width_of_current_line = 0 + + @property + def fullwidth(self): + if hasattr(self, '_terminal_width'): + return self._terminal_width + return get_terminal_width() + + @fullwidth.setter + def fullwidth(self, value): + self._terminal_width = value + + @property + def chars_on_current_line(self): + """Return the number of characters written so far in the current line. + + Please note that this count does not produce correct results after a reline() call, + see #164. + + .. versionadded:: 1.5.0 + + :rtype: int + """ + return self._chars_on_current_line + + @property + def width_of_current_line(self): + """Return an estimate of the width so far in the current line. + + .. versionadded:: 1.6.0 + + :rtype: int + """ + return self._width_of_current_line + + def _escaped(self, text, esc): + if esc and self.hasmarkup: + text = (''.join(['\x1b[%sm' % cod for cod in esc]) + + text +'\x1b[0m') + return text + + def markup(self, text, **kw): + esc = [] + for name in kw: + if name not in self._esctable: + raise ValueError("unknown markup: %r" %(name,)) + if kw[name]: + esc.append(self._esctable[name]) + return self._escaped(text, tuple(esc)) + + def sep(self, sepchar, title=None, fullwidth=None, **kw): + if fullwidth is None: + fullwidth = self.fullwidth + # the goal is to have the line be as long as possible + # under the condition that len(line) <= fullwidth + if sys.platform == "win32": + # if we print in the last column on windows we are on a + # new line but there is no way to verify/neutralize this + # (we may not know the exact line width) + # so let's be defensive to avoid empty lines in the output + fullwidth -= 1 + if title is not None: + # we want 2 + 2*len(fill) + len(title) <= fullwidth + # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth + # 2*len(sepchar)*N <= fullwidth - len(title) - 2 + # N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) + N = max((fullwidth - len(title) - 2) // (2*len(sepchar)), 1) + fill = sepchar * N + line = "%s %s %s" % (fill, title, fill) + else: + # we want len(sepchar)*N <= fullwidth + # i.e. N <= fullwidth // len(sepchar) + line = sepchar * (fullwidth // len(sepchar)) + # in some situations there is room for an extra sepchar at the right, + # in particular if we consider that with a sepchar like "_ " the + # trailing space is not important at the end of the line + if len(line) + len(sepchar.rstrip()) <= fullwidth: + line += sepchar.rstrip() + + self.line(line, **kw) + + def write(self, msg, **kw): + if msg: + if not isinstance(msg, (bytes, text)): + msg = text(msg) + + self._update_chars_on_current_line(msg) + + if self.hasmarkup and kw: + markupmsg = self.markup(msg, **kw) + else: + markupmsg = msg + write_out(self._file, markupmsg) + + def _update_chars_on_current_line(self, text_or_bytes): + newline = b'\n' if isinstance(text_or_bytes, bytes) else '\n' + current_line = text_or_bytes.rsplit(newline, 1)[-1] + if isinstance(current_line, bytes): + current_line = current_line.decode('utf-8', errors='replace') + if newline in text_or_bytes: + self._chars_on_current_line = len(current_line) + self._width_of_current_line = get_line_width(current_line) + else: + self._chars_on_current_line += len(current_line) + self._width_of_current_line += get_line_width(current_line) + + def line(self, s='', **kw): + self.write(s, **kw) + self._checkfill(s) + self.write('\n') + + def reline(self, line, **kw): + if not self.hasmarkup: + raise ValueError("cannot use rewrite-line without terminal") + self.write(line, **kw) + self._checkfill(line) + self.write('\r') + self._lastlen = len(line) + + def _checkfill(self, line): + diff2last = self._lastlen - len(line) + if diff2last > 0: + self.write(" " * diff2last) + +class Win32ConsoleWriter(TerminalWriter): + def write(self, msg, **kw): + if msg: + if not isinstance(msg, (bytes, text)): + msg = text(msg) + + self._update_chars_on_current_line(msg) + + oldcolors = None + if self.hasmarkup and kw: + handle = GetStdHandle(STD_OUTPUT_HANDLE) + oldcolors = GetConsoleInfo(handle).wAttributes + default_bg = oldcolors & 0x00F0 + attr = default_bg + if kw.pop('bold', False): + attr |= FOREGROUND_INTENSITY + + if kw.pop('red', False): + attr |= FOREGROUND_RED + elif kw.pop('blue', False): + attr |= FOREGROUND_BLUE + elif kw.pop('green', False): + attr |= FOREGROUND_GREEN + elif kw.pop('yellow', False): + attr |= FOREGROUND_GREEN|FOREGROUND_RED + else: + attr |= oldcolors & 0x0007 + + SetConsoleTextAttribute(handle, attr) + write_out(self._file, msg) + if oldcolors: + SetConsoleTextAttribute(handle, oldcolors) + +class WriteFile(object): + def __init__(self, writemethod, encoding=None): + self.encoding = encoding + self._writemethod = writemethod + + def write(self, data): + if self.encoding: + data = data.encode(self.encoding, "replace") + self._writemethod(data) + + def flush(self): + return + + +if win32_and_ctypes: + TerminalWriter = Win32ConsoleWriter + import ctypes + from ctypes import wintypes + + # ctypes access to the Windows console + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 + FOREGROUND_BLACK = 0x0000 # black text + FOREGROUND_BLUE = 0x0001 # text color contains blue. + FOREGROUND_GREEN = 0x0002 # text color contains green. + FOREGROUND_RED = 0x0004 # text color contains red. + FOREGROUND_WHITE = 0x0007 + FOREGROUND_INTENSITY = 0x0008 # text color is intensified. + BACKGROUND_BLACK = 0x0000 # background color black + BACKGROUND_BLUE = 0x0010 # background color contains blue. + BACKGROUND_GREEN = 0x0020 # background color contains green. + BACKGROUND_RED = 0x0040 # background color contains red. + BACKGROUND_WHITE = 0x0070 + BACKGROUND_INTENSITY = 0x0080 # background color is intensified. + + SHORT = ctypes.c_short + class COORD(ctypes.Structure): + _fields_ = [('X', SHORT), + ('Y', SHORT)] + class SMALL_RECT(ctypes.Structure): + _fields_ = [('Left', SHORT), + ('Top', SHORT), + ('Right', SHORT), + ('Bottom', SHORT)] + class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): + _fields_ = [('dwSize', COORD), + ('dwCursorPosition', COORD), + ('wAttributes', wintypes.WORD), + ('srWindow', SMALL_RECT), + ('dwMaximumWindowSize', COORD)] + + _GetStdHandle = ctypes.windll.kernel32.GetStdHandle + _GetStdHandle.argtypes = [wintypes.DWORD] + _GetStdHandle.restype = wintypes.HANDLE + def GetStdHandle(kind): + return _GetStdHandle(kind) + + SetConsoleTextAttribute = ctypes.windll.kernel32.SetConsoleTextAttribute + SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD] + SetConsoleTextAttribute.restype = wintypes.BOOL + + _GetConsoleScreenBufferInfo = \ + ctypes.windll.kernel32.GetConsoleScreenBufferInfo + _GetConsoleScreenBufferInfo.argtypes = [wintypes.HANDLE, + ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] + _GetConsoleScreenBufferInfo.restype = wintypes.BOOL + def GetConsoleInfo(handle): + info = CONSOLE_SCREEN_BUFFER_INFO() + _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) + return info + + def _getdimensions(): + handle = GetStdHandle(STD_OUTPUT_HANDLE) + info = GetConsoleInfo(handle) + # Substract one from the width, otherwise the cursor wraps + # and the ending \n causes an empty line to display. + return info.dwSize.Y, info.dwSize.X - 1 + +def write_out(fil, msg): + # XXX sometimes "msg" is of type bytes, sometimes text which + # complicates the situation. Should we try to enforce unicode? + try: + # on py27 and above writing out to sys.stdout with an encoding + # should usually work for unicode messages (if the encoding is + # capable of it) + fil.write(msg) + except UnicodeEncodeError: + # on py26 it might not work because stdout expects bytes + if fil.encoding: + try: + fil.write(msg.encode(fil.encoding)) + except UnicodeEncodeError: + # it might still fail if the encoding is not capable + pass + else: + fil.flush() + return + # fallback: escape all unicode characters + msg = msg.encode("unicode-escape").decode("ascii") + fil.write(msg) + fil.flush() From 3014d9a3f75f988c4925176a133efbaf75637ce1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:00:58 +0300 Subject: [PATCH 120/823] terminalwriter: auto-format --- src/_pytest/_io/terminalwriter.py | 216 ++++++++++++++++++------------ 1 file changed, 129 insertions(+), 87 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index be559867c22..aeb5c110d14 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -1,15 +1,16 @@ -""" +"""Helper functions for writing to terminals and files.""" +import sys +import os +import unicodedata -Helper functions for writing to terminals and files. - -""" +import py +from py.builtin import text, bytes +# This code was initially copied from py 1.8.1, file _io/terminalwriter.py. -import sys, os, unicodedata -import py py3k = sys.version_info[0] >= 3 py33 = sys.version_info >= (3, 3) -from py.builtin import text, bytes + win32_and_ctypes = False colorama = None @@ -19,6 +20,7 @@ except ImportError: try: import ctypes + win32_and_ctypes = True except ImportError: pass @@ -27,10 +29,14 @@ def _getdimensions(): if py33: import shutil + size = shutil.get_terminal_size() return size.lines, size.columns else: - import termios, fcntl, struct + import termios + import fcntl + import struct + call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8) height, width = struct.unpack("hhhh", call)[:2] return height, width @@ -50,27 +56,28 @@ def get_terminal_width(): # FALLBACK: # * some exception happened # * or this is emacs terminal which reports (0,0) - width = int(os.environ.get('COLUMNS', 80)) + width = int(os.environ.get("COLUMNS", 80)) # XXX the windows getdimensions may be bogus, let's sanify a bit if width < 40: width = 80 return width + terminal_width = get_terminal_width() char_width = { - 'A': 1, # "Ambiguous" - 'F': 2, # Fullwidth - 'H': 1, # Halfwidth - 'N': 1, # Neutral - 'Na': 1, # Narrow - 'W': 2, # Wide + "A": 1, # "Ambiguous" + "F": 2, # Fullwidth + "H": 1, # Halfwidth + "N": 1, # Neutral + "Na": 1, # Narrow + "W": 2, # Wide } def get_line_width(text): - text = unicodedata.normalize('NFC', text) + text = unicodedata.normalize("NFC", text) return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text) @@ -82,11 +89,11 @@ def ansi_print(text, esc, file=None, newline=True, flush=False): if esc and not isinstance(esc, tuple): esc = (esc,) if esc and sys.platform != "win32" and file.isatty(): - text = (''.join(['\x1b[%sm' % cod for cod in esc]) + - text + - '\x1b[0m') # ANSI color code "reset" + text = ( + "".join(["\x1b[%sm" % cod for cod in esc]) + text + "\x1b[0m" + ) # ANSI color code "reset" if newline: - text += '\n' + text += "\n" if esc and win32_and_ctypes and file.isatty(): if 1 in esc: @@ -94,16 +101,17 @@ def ansi_print(text, esc, file=None, newline=True, flush=False): esc = tuple([x for x in esc if x != 1]) else: bold = False - esctable = {() : FOREGROUND_WHITE, # normal - (31,): FOREGROUND_RED, # red - (32,): FOREGROUND_GREEN, # green - (33,): FOREGROUND_GREEN|FOREGROUND_RED, # yellow - (34,): FOREGROUND_BLUE, # blue - (35,): FOREGROUND_BLUE|FOREGROUND_RED, # purple - (36,): FOREGROUND_BLUE|FOREGROUND_GREEN, # cyan - (37,): FOREGROUND_WHITE, # white - (39,): FOREGROUND_WHITE, # reset - } + esctable = { + (): FOREGROUND_WHITE, # normal + (31,): FOREGROUND_RED, # red + (32,): FOREGROUND_GREEN, # green + (33,): FOREGROUND_GREEN | FOREGROUND_RED, # yellow + (34,): FOREGROUND_BLUE, # blue + (35,): FOREGROUND_BLUE | FOREGROUND_RED, # purple + (36,): FOREGROUND_BLUE | FOREGROUND_GREEN, # cyan + (37,): FOREGROUND_WHITE, # white + (39,): FOREGROUND_WHITE, # reset + } attr = esctable.get(esc, FOREGROUND_WHITE) if bold: attr |= FOREGROUND_INTENSITY @@ -114,7 +122,7 @@ def ansi_print(text, esc, file=None, newline=True, flush=False): else: handle = GetStdHandle(STD_OUTPUT_HANDLE) oldcolors = GetConsoleInfo(handle).wAttributes - attr |= (oldcolors & 0x0f0) + attr |= oldcolors & 0x0F0 SetConsoleTextAttribute(handle, attr) while len(text) > 32768: file.write(text[:32768]) @@ -128,21 +136,43 @@ def ansi_print(text, esc, file=None, newline=True, flush=False): if flush: file.flush() + def should_do_markup(file): - if os.environ.get('PY_COLORS') == '1': + if os.environ.get("PY_COLORS") == "1": return True - if os.environ.get('PY_COLORS') == '0': + if os.environ.get("PY_COLORS") == "0": return False - return hasattr(file, 'isatty') and file.isatty() \ - and os.environ.get('TERM') != 'dumb' \ - and not (sys.platform.startswith('java') and os._name == 'nt') + return ( + hasattr(file, "isatty") + and file.isatty() + and os.environ.get("TERM") != "dumb" + and not (sys.platform.startswith("java") and os._name == "nt") + ) + class TerminalWriter(object): - _esctable = dict(black=30, red=31, green=32, yellow=33, - blue=34, purple=35, cyan=36, white=37, - Black=40, Red=41, Green=42, Yellow=43, - Blue=44, Purple=45, Cyan=46, White=47, - bold=1, light=2, blink=5, invert=7) + _esctable = dict( + black=30, + red=31, + green=32, + yellow=33, + blue=34, + purple=35, + cyan=36, + white=37, + Black=40, + Red=41, + Green=42, + Yellow=43, + Blue=44, + Purple=45, + Cyan=46, + White=47, + bold=1, + light=2, + blink=5, + invert=7, + ) # XXX deprecate stringio argument def __init__(self, file=None, stringio=False, encoding=None): @@ -152,11 +182,12 @@ def __init__(self, file=None, stringio=False, encoding=None): else: from sys import stdout as file elif py.builtin.callable(file) and not ( - hasattr(file, "write") and hasattr(file, "flush")): + hasattr(file, "write") and hasattr(file, "flush") + ): file = WriteFile(file, encoding=encoding) if hasattr(file, "isatty") and file.isatty() and colorama: file = colorama.AnsiToWin32(file).stream - self.encoding = encoding or getattr(file, 'encoding', "utf-8") + self.encoding = encoding or getattr(file, "encoding", "utf-8") self._file = file self.hasmarkup = should_do_markup(file) self._lastlen = 0 @@ -165,7 +196,7 @@ def __init__(self, file=None, stringio=False, encoding=None): @property def fullwidth(self): - if hasattr(self, '_terminal_width'): + if hasattr(self, "_terminal_width"): return self._terminal_width return get_terminal_width() @@ -198,15 +229,14 @@ def width_of_current_line(self): def _escaped(self, text, esc): if esc and self.hasmarkup: - text = (''.join(['\x1b[%sm' % cod for cod in esc]) + - text +'\x1b[0m') + text = "".join(["\x1b[%sm" % cod for cod in esc]) + text + "\x1b[0m" return text def markup(self, text, **kw): esc = [] for name in kw: if name not in self._esctable: - raise ValueError("unknown markup: %r" %(name,)) + raise ValueError("unknown markup: %r" % (name,)) if kw[name]: esc.append(self._esctable[name]) return self._escaped(text, tuple(esc)) @@ -227,7 +257,7 @@ def sep(self, sepchar, title=None, fullwidth=None, **kw): # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth # 2*len(sepchar)*N <= fullwidth - len(title) - 2 # N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) - N = max((fullwidth - len(title) - 2) // (2*len(sepchar)), 1) + N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1) fill = sepchar * N line = "%s %s %s" % (fill, title, fill) else: @@ -256,10 +286,10 @@ def write(self, msg, **kw): write_out(self._file, markupmsg) def _update_chars_on_current_line(self, text_or_bytes): - newline = b'\n' if isinstance(text_or_bytes, bytes) else '\n' + newline = b"\n" if isinstance(text_or_bytes, bytes) else "\n" current_line = text_or_bytes.rsplit(newline, 1)[-1] if isinstance(current_line, bytes): - current_line = current_line.decode('utf-8', errors='replace') + current_line = current_line.decode("utf-8", errors="replace") if newline in text_or_bytes: self._chars_on_current_line = len(current_line) self._width_of_current_line = get_line_width(current_line) @@ -267,17 +297,17 @@ def _update_chars_on_current_line(self, text_or_bytes): self._chars_on_current_line += len(current_line) self._width_of_current_line += get_line_width(current_line) - def line(self, s='', **kw): + def line(self, s="", **kw): self.write(s, **kw) self._checkfill(s) - self.write('\n') + self.write("\n") def reline(self, line, **kw): if not self.hasmarkup: raise ValueError("cannot use rewrite-line without terminal") self.write(line, **kw) self._checkfill(line) - self.write('\r') + self.write("\r") self._lastlen = len(line) def _checkfill(self, line): @@ -285,6 +315,7 @@ def _checkfill(self, line): if diff2last > 0: self.write(" " * diff2last) + class Win32ConsoleWriter(TerminalWriter): def write(self, msg, **kw): if msg: @@ -299,17 +330,17 @@ def write(self, msg, **kw): oldcolors = GetConsoleInfo(handle).wAttributes default_bg = oldcolors & 0x00F0 attr = default_bg - if kw.pop('bold', False): + if kw.pop("bold", False): attr |= FOREGROUND_INTENSITY - if kw.pop('red', False): + if kw.pop("red", False): attr |= FOREGROUND_RED - elif kw.pop('blue', False): + elif kw.pop("blue", False): attr |= FOREGROUND_BLUE - elif kw.pop('green', False): + elif kw.pop("green", False): attr |= FOREGROUND_GREEN - elif kw.pop('yellow', False): - attr |= FOREGROUND_GREEN|FOREGROUND_RED + elif kw.pop("yellow", False): + attr |= FOREGROUND_GREEN | FOREGROUND_RED else: attr |= oldcolors & 0x0007 @@ -318,6 +349,7 @@ def write(self, msg, **kw): if oldcolors: SetConsoleTextAttribute(handle, oldcolors) + class WriteFile(object): def __init__(self, writemethod, encoding=None): self.encoding = encoding @@ -339,39 +371,46 @@ def flush(self): # ctypes access to the Windows console STD_OUTPUT_HANDLE = -11 - STD_ERROR_HANDLE = -12 - FOREGROUND_BLACK = 0x0000 # black text - FOREGROUND_BLUE = 0x0001 # text color contains blue. - FOREGROUND_GREEN = 0x0002 # text color contains green. - FOREGROUND_RED = 0x0004 # text color contains red. - FOREGROUND_WHITE = 0x0007 - FOREGROUND_INTENSITY = 0x0008 # text color is intensified. - BACKGROUND_BLACK = 0x0000 # background color black - BACKGROUND_BLUE = 0x0010 # background color contains blue. - BACKGROUND_GREEN = 0x0020 # background color contains green. - BACKGROUND_RED = 0x0040 # background color contains red. - BACKGROUND_WHITE = 0x0070 - BACKGROUND_INTENSITY = 0x0080 # background color is intensified. + STD_ERROR_HANDLE = -12 + FOREGROUND_BLACK = 0x0000 # black text + FOREGROUND_BLUE = 0x0001 # text color contains blue. + FOREGROUND_GREEN = 0x0002 # text color contains green. + FOREGROUND_RED = 0x0004 # text color contains red. + FOREGROUND_WHITE = 0x0007 + FOREGROUND_INTENSITY = 0x0008 # text color is intensified. + BACKGROUND_BLACK = 0x0000 # background color black + BACKGROUND_BLUE = 0x0010 # background color contains blue. + BACKGROUND_GREEN = 0x0020 # background color contains green. + BACKGROUND_RED = 0x0040 # background color contains red. + BACKGROUND_WHITE = 0x0070 + BACKGROUND_INTENSITY = 0x0080 # background color is intensified. SHORT = ctypes.c_short + class COORD(ctypes.Structure): - _fields_ = [('X', SHORT), - ('Y', SHORT)] + _fields_ = [("X", SHORT), ("Y", SHORT)] + class SMALL_RECT(ctypes.Structure): - _fields_ = [('Left', SHORT), - ('Top', SHORT), - ('Right', SHORT), - ('Bottom', SHORT)] + _fields_ = [ + ("Left", SHORT), + ("Top", SHORT), + ("Right", SHORT), + ("Bottom", SHORT), + ] + class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): - _fields_ = [('dwSize', COORD), - ('dwCursorPosition', COORD), - ('wAttributes', wintypes.WORD), - ('srWindow', SMALL_RECT), - ('dwMaximumWindowSize', COORD)] + _fields_ = [ + ("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", wintypes.WORD), + ("srWindow", SMALL_RECT), + ("dwMaximumWindowSize", COORD), + ] _GetStdHandle = ctypes.windll.kernel32.GetStdHandle _GetStdHandle.argtypes = [wintypes.DWORD] _GetStdHandle.restype = wintypes.HANDLE + def GetStdHandle(kind): return _GetStdHandle(kind) @@ -379,11 +418,13 @@ def GetStdHandle(kind): SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD] SetConsoleTextAttribute.restype = wintypes.BOOL - _GetConsoleScreenBufferInfo = \ - ctypes.windll.kernel32.GetConsoleScreenBufferInfo - _GetConsoleScreenBufferInfo.argtypes = [wintypes.HANDLE, - ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] + _GetConsoleScreenBufferInfo = ctypes.windll.kernel32.GetConsoleScreenBufferInfo + _GetConsoleScreenBufferInfo.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), + ] _GetConsoleScreenBufferInfo.restype = wintypes.BOOL + def GetConsoleInfo(handle): info = CONSOLE_SCREEN_BUFFER_INFO() _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) @@ -396,6 +437,7 @@ def _getdimensions(): # and the ending \n causes an empty line to display. return info.dwSize.Y, info.dwSize.X - 1 + def write_out(fil, msg): # XXX sometimes "msg" is of type bytes, sometimes text which # complicates the situation. Should we try to enforce unicode? From 5e2d820308a0a78212efb6dadfdc7cbe02b7cbec Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:54:58 +0300 Subject: [PATCH 121/823] terminalwriter: fix lints --- src/_pytest/_io/terminalwriter.py | 68 ++++++++++++------------------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index aeb5c110d14..609d4418c6d 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -1,16 +1,13 @@ """Helper functions for writing to terminals and files.""" -import sys import os +import shutil +import sys import unicodedata +from io import StringIO -import py -from py.builtin import text, bytes # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. -py3k = sys.version_info[0] >= 3 -py33 = sys.version_info >= (3, 3) - win32_and_ctypes = False colorama = None @@ -20,35 +17,24 @@ except ImportError: try: import ctypes - - win32_and_ctypes = True except ImportError: pass + else: + win32_and_ctypes = True def _getdimensions(): - if py33: - import shutil - - size = shutil.get_terminal_size() - return size.lines, size.columns - else: - import termios - import fcntl - import struct - - call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8) - height, width = struct.unpack("hhhh", call)[:2] - return height, width + size = shutil.get_terminal_size() + return size.lines, size.columns def get_terminal_width(): width = 0 try: _, width = _getdimensions() - except py.builtin._sysex: + except (KeyboardInterrupt, SystemExit, MemoryError, GeneratorExit): raise - except: + except BaseException: # pass to fallback below pass @@ -150,7 +136,7 @@ def should_do_markup(file): ) -class TerminalWriter(object): +class TerminalWriter: _esctable = dict( black=30, red=31, @@ -178,12 +164,10 @@ class TerminalWriter(object): def __init__(self, file=None, stringio=False, encoding=None): if file is None: if stringio: - self.stringio = file = py.io.TextIO() + self.stringio = file = StringIO() else: from sys import stdout as file - elif py.builtin.callable(file) and not ( - hasattr(file, "write") and hasattr(file, "flush") - ): + elif callable(file) and not (hasattr(file, "write") and hasattr(file, "flush")): file = WriteFile(file, encoding=encoding) if hasattr(file, "isatty") and file.isatty() and colorama: file = colorama.AnsiToWin32(file).stream @@ -236,7 +220,7 @@ def markup(self, text, **kw): esc = [] for name in kw: if name not in self._esctable: - raise ValueError("unknown markup: %r" % (name,)) + raise ValueError("unknown markup: {!r}".format(name)) if kw[name]: esc.append(self._esctable[name]) return self._escaped(text, tuple(esc)) @@ -259,7 +243,7 @@ def sep(self, sepchar, title=None, fullwidth=None, **kw): # N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1) fill = sepchar * N - line = "%s %s %s" % (fill, title, fill) + line = "{} {} {}".format(fill, title, fill) else: # we want len(sepchar)*N <= fullwidth # i.e. N <= fullwidth // len(sepchar) @@ -274,8 +258,8 @@ def sep(self, sepchar, title=None, fullwidth=None, **kw): def write(self, msg, **kw): if msg: - if not isinstance(msg, (bytes, text)): - msg = text(msg) + if not isinstance(msg, (bytes, str)): + msg = str(msg) self._update_chars_on_current_line(msg) @@ -319,8 +303,8 @@ def _checkfill(self, line): class Win32ConsoleWriter(TerminalWriter): def write(self, msg, **kw): if msg: - if not isinstance(msg, (bytes, text)): - msg = text(msg) + if not isinstance(msg, (bytes, str)): + msg = str(msg) self._update_chars_on_current_line(msg) @@ -350,7 +334,7 @@ def write(self, msg, **kw): SetConsoleTextAttribute(handle, oldcolors) -class WriteFile(object): +class WriteFile: def __init__(self, writemethod, encoding=None): self.encoding = encoding self._writemethod = writemethod @@ -365,9 +349,11 @@ def flush(self): if win32_and_ctypes: - TerminalWriter = Win32ConsoleWriter - import ctypes + import ctypes # noqa: F811 from ctypes import wintypes + from ctypes import windll # type: ignore[attr-defined] # noqa: F821 + + TerminalWriter = Win32ConsoleWriter # type: ignore[misc] # noqa: F821 # ctypes access to the Windows console STD_OUTPUT_HANDLE = -11 @@ -407,18 +393,18 @@ class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): ("dwMaximumWindowSize", COORD), ] - _GetStdHandle = ctypes.windll.kernel32.GetStdHandle + _GetStdHandle = windll.kernel32.GetStdHandle _GetStdHandle.argtypes = [wintypes.DWORD] _GetStdHandle.restype = wintypes.HANDLE def GetStdHandle(kind): return _GetStdHandle(kind) - SetConsoleTextAttribute = ctypes.windll.kernel32.SetConsoleTextAttribute + SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD] SetConsoleTextAttribute.restype = wintypes.BOOL - _GetConsoleScreenBufferInfo = ctypes.windll.kernel32.GetConsoleScreenBufferInfo + _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo _GetConsoleScreenBufferInfo.argtypes = [ wintypes.HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), @@ -430,7 +416,7 @@ def GetConsoleInfo(handle): _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) return info - def _getdimensions(): + def _getdimensions(): # noqa: F811 handle = GetStdHandle(STD_OUTPUT_HANDLE) info = GetConsoleInfo(handle) # Substract one from the width, otherwise the cursor wraps From 1d596b27a707414145793b16ad1b1f2ffbc02799 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:17:44 +0300 Subject: [PATCH 122/823] terminalwriter: move Win32ConsoleWriter definition under win32 conditional This way non-Windows platforms skip it. It also uses things defined inside the `if`. --- src/_pytest/_io/terminalwriter.py | 67 +++++++++++++++---------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 609d4418c6d..17740e83e37 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -300,40 +300,6 @@ def _checkfill(self, line): self.write(" " * diff2last) -class Win32ConsoleWriter(TerminalWriter): - def write(self, msg, **kw): - if msg: - if not isinstance(msg, (bytes, str)): - msg = str(msg) - - self._update_chars_on_current_line(msg) - - oldcolors = None - if self.hasmarkup and kw: - handle = GetStdHandle(STD_OUTPUT_HANDLE) - oldcolors = GetConsoleInfo(handle).wAttributes - default_bg = oldcolors & 0x00F0 - attr = default_bg - if kw.pop("bold", False): - attr |= FOREGROUND_INTENSITY - - if kw.pop("red", False): - attr |= FOREGROUND_RED - elif kw.pop("blue", False): - attr |= FOREGROUND_BLUE - elif kw.pop("green", False): - attr |= FOREGROUND_GREEN - elif kw.pop("yellow", False): - attr |= FOREGROUND_GREEN | FOREGROUND_RED - else: - attr |= oldcolors & 0x0007 - - SetConsoleTextAttribute(handle, attr) - write_out(self._file, msg) - if oldcolors: - SetConsoleTextAttribute(handle, oldcolors) - - class WriteFile: def __init__(self, writemethod, encoding=None): self.encoding = encoding @@ -353,6 +319,39 @@ def flush(self): from ctypes import wintypes from ctypes import windll # type: ignore[attr-defined] # noqa: F821 + class Win32ConsoleWriter(TerminalWriter): + def write(self, msg, **kw): + if msg: + if not isinstance(msg, (bytes, str)): + msg = str(msg) + + self._update_chars_on_current_line(msg) + + oldcolors = None + if self.hasmarkup and kw: + handle = GetStdHandle(STD_OUTPUT_HANDLE) + oldcolors = GetConsoleInfo(handle).wAttributes + default_bg = oldcolors & 0x00F0 + attr = default_bg + if kw.pop("bold", False): + attr |= FOREGROUND_INTENSITY + + if kw.pop("red", False): + attr |= FOREGROUND_RED + elif kw.pop("blue", False): + attr |= FOREGROUND_BLUE + elif kw.pop("green", False): + attr |= FOREGROUND_GREEN + elif kw.pop("yellow", False): + attr |= FOREGROUND_GREEN | FOREGROUND_RED + else: + attr |= oldcolors & 0x0007 + + SetConsoleTextAttribute(handle, attr) + write_out(self._file, msg) + if oldcolors: + SetConsoleTextAttribute(handle, oldcolors) + TerminalWriter = Win32ConsoleWriter # type: ignore[misc] # noqa: F821 # ctypes access to the Windows console From c749e44efc37c2792b0d1257930a24c74b4a7251 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:37:38 +0300 Subject: [PATCH 123/823] terminalwriter: remove custom win32 screen width code Python 3 does this on its own so we can use the shared code: https://github.com/python/cpython/commit/bcf2b59fb5f18c09a26da3e9b60a37367f2a28ba --- src/_pytest/_io/terminalwriter.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 17740e83e37..6ff39111a06 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -415,13 +415,6 @@ def GetConsoleInfo(handle): _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) return info - def _getdimensions(): # noqa: F811 - handle = GetStdHandle(STD_OUTPUT_HANDLE) - info = GetConsoleInfo(handle) - # Substract one from the width, otherwise the cursor wraps - # and the ending \n causes an empty line to display. - return info.dwSize.Y, info.dwSize.X - 1 - def write_out(fil, msg): # XXX sometimes "msg" is of type bytes, sometimes text which From 6c1b6a09b878ea74796618d02e7a8222464e1b9a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:12:32 +0300 Subject: [PATCH 124/823] terminalwriter: simplify get_terminal_width() The shutil.get_terminal_size() handles everything this did already. --- src/_pytest/_io/terminalwriter.py | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 6ff39111a06..5533aa91446 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -23,34 +23,15 @@ win32_and_ctypes = True -def _getdimensions(): - size = shutil.get_terminal_size() - return size.lines, size.columns +def get_terminal_width() -> int: + width, _ = shutil.get_terminal_size(fallback=(80, 24)) - -def get_terminal_width(): - width = 0 - try: - _, width = _getdimensions() - except (KeyboardInterrupt, SystemExit, MemoryError, GeneratorExit): - raise - except BaseException: - # pass to fallback below - pass - - if width == 0: - # FALLBACK: - # * some exception happened - # * or this is emacs terminal which reports (0,0) - width = int(os.environ.get("COLUMNS", 80)) - - # XXX the windows getdimensions may be bogus, let's sanify a bit + # The Windows get_terminal_size may be bogus, let's sanify a bit. if width < 40: width = 80 - return width + return width -terminal_width = get_terminal_width() char_width = { "A": 1, # "Ambiguous" From 9a59970cad80114aeb24ffcd869defc127cb6173 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:28:26 +0300 Subject: [PATCH 125/823] terminalwriter: optimize get_line_width() a bit This function is called a lot when printing a lot of text, and is very slow -- this speeds it up a bit. --- src/_pytest/_io/terminalwriter.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 5533aa91446..17a67bddb5e 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -3,6 +3,7 @@ import shutil import sys import unicodedata +from functools import lru_cache from io import StringIO @@ -33,19 +34,15 @@ def get_terminal_width() -> int: return width -char_width = { - "A": 1, # "Ambiguous" - "F": 2, # Fullwidth - "H": 1, # Halfwidth - "N": 1, # Neutral - "Na": 1, # Narrow - "W": 2, # Wide -} +@lru_cache(100) +def char_width(c: str) -> int: + # Fullwidth and Wide -> 2, all else (including Ambiguous) -> 1. + return 2 if unicodedata.east_asian_width(c) in ("F", "W") else 1 def get_line_width(text): text = unicodedata.normalize("NFC", text) - return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text) + return sum(char_width(c) for c in text) # XXX unify with _escaped func below From b6cc90e0afe90c84d84c5b15a2db75d87a2681d7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:45:10 +0300 Subject: [PATCH 126/823] terminalwriter: remove support for writing bytes directly It is not used and slows things down. --- src/_pytest/_io/terminalwriter.py | 58 +++++++------------------------ 1 file changed, 13 insertions(+), 45 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 17a67bddb5e..0e7f0ccff72 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -234,37 +234,32 @@ def sep(self, sepchar, title=None, fullwidth=None, **kw): self.line(line, **kw) - def write(self, msg, **kw): + def write(self, msg: str, **kw) -> None: if msg: - if not isinstance(msg, (bytes, str)): - msg = str(msg) - self._update_chars_on_current_line(msg) if self.hasmarkup and kw: markupmsg = self.markup(msg, **kw) else: markupmsg = msg - write_out(self._file, markupmsg) - - def _update_chars_on_current_line(self, text_or_bytes): - newline = b"\n" if isinstance(text_or_bytes, bytes) else "\n" - current_line = text_or_bytes.rsplit(newline, 1)[-1] - if isinstance(current_line, bytes): - current_line = current_line.decode("utf-8", errors="replace") - if newline in text_or_bytes: + self._file.write(markupmsg) + self._file.flush() + + def _update_chars_on_current_line(self, text: str) -> None: + current_line = text.rsplit("\n", 1)[-1] + if "\n" in text: self._chars_on_current_line = len(current_line) self._width_of_current_line = get_line_width(current_line) else: self._chars_on_current_line += len(current_line) self._width_of_current_line += get_line_width(current_line) - def line(self, s="", **kw): + def line(self, s: str = "", **kw): self.write(s, **kw) self._checkfill(s) self.write("\n") - def reline(self, line, **kw): + def reline(self, line: str, **kw): if not self.hasmarkup: raise ValueError("cannot use rewrite-line without terminal") self.write(line, **kw) @@ -272,7 +267,7 @@ def reline(self, line, **kw): self.write("\r") self._lastlen = len(line) - def _checkfill(self, line): + def _checkfill(self, line: str) -> None: diff2last = self._lastlen - len(line) if diff2last > 0: self.write(" " * diff2last) @@ -298,11 +293,8 @@ def flush(self): from ctypes import windll # type: ignore[attr-defined] # noqa: F821 class Win32ConsoleWriter(TerminalWriter): - def write(self, msg, **kw): + def write(self, msg: str, **kw): if msg: - if not isinstance(msg, (bytes, str)): - msg = str(msg) - self._update_chars_on_current_line(msg) oldcolors = None @@ -326,7 +318,8 @@ def write(self, msg, **kw): attr |= oldcolors & 0x0007 SetConsoleTextAttribute(handle, attr) - write_out(self._file, msg) + self._file.write(msg) + self._file.flush() if oldcolors: SetConsoleTextAttribute(handle, oldcolors) @@ -392,28 +385,3 @@ def GetConsoleInfo(handle): info = CONSOLE_SCREEN_BUFFER_INFO() _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) return info - - -def write_out(fil, msg): - # XXX sometimes "msg" is of type bytes, sometimes text which - # complicates the situation. Should we try to enforce unicode? - try: - # on py27 and above writing out to sys.stdout with an encoding - # should usually work for unicode messages (if the encoding is - # capable of it) - fil.write(msg) - except UnicodeEncodeError: - # on py26 it might not work because stdout expects bytes - if fil.encoding: - try: - fil.write(msg.encode(fil.encoding)) - except UnicodeEncodeError: - # it might still fail if the encoding is not capable - pass - else: - fil.flush() - return - # fallback: escape all unicode characters - msg = msg.encode("unicode-escape").decode("ascii") - fil.write(msg) - fil.flush() From a6819726cdaf05a479fcc5f74d2f091f85b672c7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:51:14 +0300 Subject: [PATCH 127/823] terminalwriter: remove unused function ansi_print --- src/_pytest/_io/terminalwriter.py | 56 ------------------------------- 1 file changed, 56 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 0e7f0ccff72..6e77f2ebfd1 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -45,62 +45,6 @@ def get_line_width(text): return sum(char_width(c) for c in text) -# XXX unify with _escaped func below -def ansi_print(text, esc, file=None, newline=True, flush=False): - if file is None: - file = sys.stderr - text = text.rstrip() - if esc and not isinstance(esc, tuple): - esc = (esc,) - if esc and sys.platform != "win32" and file.isatty(): - text = ( - "".join(["\x1b[%sm" % cod for cod in esc]) + text + "\x1b[0m" - ) # ANSI color code "reset" - if newline: - text += "\n" - - if esc and win32_and_ctypes and file.isatty(): - if 1 in esc: - bold = True - esc = tuple([x for x in esc if x != 1]) - else: - bold = False - esctable = { - (): FOREGROUND_WHITE, # normal - (31,): FOREGROUND_RED, # red - (32,): FOREGROUND_GREEN, # green - (33,): FOREGROUND_GREEN | FOREGROUND_RED, # yellow - (34,): FOREGROUND_BLUE, # blue - (35,): FOREGROUND_BLUE | FOREGROUND_RED, # purple - (36,): FOREGROUND_BLUE | FOREGROUND_GREEN, # cyan - (37,): FOREGROUND_WHITE, # white - (39,): FOREGROUND_WHITE, # reset - } - attr = esctable.get(esc, FOREGROUND_WHITE) - if bold: - attr |= FOREGROUND_INTENSITY - STD_OUTPUT_HANDLE = -11 - STD_ERROR_HANDLE = -12 - if file is sys.stderr: - handle = GetStdHandle(STD_ERROR_HANDLE) - else: - handle = GetStdHandle(STD_OUTPUT_HANDLE) - oldcolors = GetConsoleInfo(handle).wAttributes - attr |= oldcolors & 0x0F0 - SetConsoleTextAttribute(handle, attr) - while len(text) > 32768: - file.write(text[:32768]) - text = text[32768:] - if text: - file.write(text) - SetConsoleTextAttribute(handle, oldcolors) - else: - file.write(text) - - if flush: - file.flush() - - def should_do_markup(file): if os.environ.get("PY_COLORS") == "1": return True From 0528307ebfc08aa8b826d2905c827fcdd6a86419 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:15:23 +0300 Subject: [PATCH 128/823] terminalwriter: remove unused function TerminalWriter.reline --- src/_pytest/_io/terminalwriter.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 6e77f2ebfd1..c11eb9aba3e 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -96,7 +96,6 @@ def __init__(self, file=None, stringio=False, encoding=None): self.encoding = encoding or getattr(file, "encoding", "utf-8") self._file = file self.hasmarkup = should_do_markup(file) - self._lastlen = 0 self._chars_on_current_line = 0 self._width_of_current_line = 0 @@ -114,11 +113,6 @@ def fullwidth(self, value): def chars_on_current_line(self): """Return the number of characters written so far in the current line. - Please note that this count does not produce correct results after a reline() call, - see #164. - - .. versionadded:: 1.5.0 - :rtype: int """ return self._chars_on_current_line @@ -127,8 +121,6 @@ def chars_on_current_line(self): def width_of_current_line(self): """Return an estimate of the width so far in the current line. - .. versionadded:: 1.6.0 - :rtype: int """ return self._width_of_current_line @@ -200,22 +192,8 @@ def _update_chars_on_current_line(self, text: str) -> None: def line(self, s: str = "", **kw): self.write(s, **kw) - self._checkfill(s) self.write("\n") - def reline(self, line: str, **kw): - if not self.hasmarkup: - raise ValueError("cannot use rewrite-line without terminal") - self.write(line, **kw) - self._checkfill(line) - self.write("\r") - self._lastlen = len(line) - - def _checkfill(self, line: str) -> None: - diff2last = self._lastlen - len(line) - if diff2last > 0: - self.write(" " * diff2last) - class WriteFile: def __init__(self, writemethod, encoding=None): From dac05ccd9a290ac232d933dfb2c4b0f651468867 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 15:57:21 +0300 Subject: [PATCH 129/823] terminalwriter: remove support for passing callable as file in TerminalWriter Not used. --- src/_pytest/_io/terminalwriter.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index c11eb9aba3e..437c96794dd 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -83,17 +83,14 @@ class TerminalWriter: ) # XXX deprecate stringio argument - def __init__(self, file=None, stringio=False, encoding=None): + def __init__(self, file=None, stringio=False): if file is None: if stringio: self.stringio = file = StringIO() else: from sys import stdout as file - elif callable(file) and not (hasattr(file, "write") and hasattr(file, "flush")): - file = WriteFile(file, encoding=encoding) if hasattr(file, "isatty") and file.isatty() and colorama: file = colorama.AnsiToWin32(file).stream - self.encoding = encoding or getattr(file, "encoding", "utf-8") self._file = file self.hasmarkup = should_do_markup(file) self._chars_on_current_line = 0 @@ -195,20 +192,6 @@ def line(self, s: str = "", **kw): self.write("\n") -class WriteFile: - def __init__(self, writemethod, encoding=None): - self.encoding = encoding - self._writemethod = writemethod - - def write(self, data): - if self.encoding: - data = data.encode(self.encoding, "replace") - self._writemethod(data) - - def flush(self): - return - - if win32_and_ctypes: import ctypes # noqa: F811 from ctypes import wintypes From 94a57d235322588ccde609bd9c2c384f0e81a337 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:04:57 +0300 Subject: [PATCH 130/823] io: combine _io.TerminalWriter and _io.terminalwriter.TerminalWriter Previously it extended an external type but now it come move to the type itself. --- src/_pytest/_io/__init__.py | 41 +++---------------------------- src/_pytest/_io/terminalwriter.py | 34 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index d401cda8e13..880c3c87a9f 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -1,39 +1,6 @@ -from typing import List -from typing import Sequence +from .terminalwriter import TerminalWriter -from .terminalwriter import TerminalWriter as BaseTerminalWriter - -class TerminalWriter(BaseTerminalWriter): - def _write_source(self, lines: List[str], indents: Sequence[str] = ()) -> None: - """Write lines of source code possibly highlighted. - - Keeping this private for now because the API is clunky. We should discuss how - to evolve the terminal writer so we can have more precise color support, for example - being able to write part of a line in one color and the rest in another, and so on. - """ - if indents and len(indents) != len(lines): - raise ValueError( - "indents size ({}) should have same size as lines ({})".format( - len(indents), len(lines) - ) - ) - if not indents: - indents = [""] * len(lines) - source = "\n".join(lines) - new_lines = self._highlight(source).splitlines() - for indent, new_line in zip(indents, new_lines): - self.line(indent + new_line) - - def _highlight(self, source): - """Highlight the given source code if we have markup support""" - if not self.hasmarkup: - return source - try: - from pygments.formatters.terminal import TerminalFormatter - from pygments.lexers.python import PythonLexer - from pygments import highlight - except ImportError: - return source - else: - return highlight(source, PythonLexer(), TerminalFormatter(bg="dark")) +__all__ = [ + "TerminalWriter", +] diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 437c96794dd..e4e5db22841 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -5,6 +5,7 @@ import unicodedata from functools import lru_cache from io import StringIO +from typing import Sequence # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. @@ -191,6 +192,39 @@ def line(self, s: str = "", **kw): self.write(s, **kw) self.write("\n") + def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None: + """Write lines of source code possibly highlighted. + + Keeping this private for now because the API is clunky. We should discuss how + to evolve the terminal writer so we can have more precise color support, for example + being able to write part of a line in one color and the rest in another, and so on. + """ + if indents and len(indents) != len(lines): + raise ValueError( + "indents size ({}) should have same size as lines ({})".format( + len(indents), len(lines) + ) + ) + if not indents: + indents = [""] * len(lines) + source = "\n".join(lines) + new_lines = self._highlight(source).splitlines() + for indent, new_line in zip(indents, new_lines): + self.line(indent + new_line) + + def _highlight(self, source): + """Highlight the given source code if we have markup support""" + if not self.hasmarkup: + return source + try: + from pygments.formatters.terminal import TerminalFormatter + from pygments.lexers.python import PythonLexer + from pygments import highlight + except ImportError: + return source + else: + return highlight(source, PythonLexer(), TerminalFormatter(bg="dark")) + if win32_and_ctypes: import ctypes # noqa: F811 From 66ee7556494dea45c70b8cd2dfac523653b83b4f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:45:47 +0300 Subject: [PATCH 131/823] terminalwriter: remove TerminalWriter's stringio argument Had a mark indicating it should be removed, and I agree, it's better to just use the `file` argument. --- src/_pytest/_io/terminalwriter.py | 9 ++------- src/_pytest/pastebin.py | 8 ++++---- src/_pytest/reports.py | 5 +++-- testing/code/test_excinfo.py | 11 +++++++---- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index e4e5db22841..907b905436d 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -4,7 +4,6 @@ import sys import unicodedata from functools import lru_cache -from io import StringIO from typing import Sequence @@ -83,13 +82,9 @@ class TerminalWriter: invert=7, ) - # XXX deprecate stringio argument - def __init__(self, file=None, stringio=False): + def __init__(self, file=None): if file is None: - if stringio: - self.stringio = file = StringIO() - else: - from sys import stdout as file + file = sys.stdout if hasattr(file, "isatty") and file.isatty() and colorama: file = colorama.AnsiToWin32(file).stream self._file = file diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 3f4a7502d59..cbaa9a9f5f1 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -1,5 +1,6 @@ """ submit failure or test session information to a pastebin service. """ import tempfile +from io import StringIO from typing import IO import pytest @@ -99,11 +100,10 @@ def pytest_terminal_summary(terminalreporter): msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc except AttributeError: msg = tr._getfailureheadline(rep) - tw = _pytest.config.create_terminal_writer( - terminalreporter.config, stringio=True - ) + file = StringIO() + tw = _pytest.config.create_terminal_writer(terminalreporter.config, file) rep.toterminal(tw) - s = tw.stringio.getvalue() + s = file.getvalue() assert len(s) pastebinurl = create_new_paste(s) tr.write_line("{} --> {}".format(msg, pastebinurl)) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 8459c1cb9e4..178df6004f2 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -82,10 +82,11 @@ def longreprtext(self): .. versionadded:: 3.0 """ - tw = TerminalWriter(stringio=True) + file = StringIO() + tw = TerminalWriter(file) tw.hasmarkup = False self.toterminal(tw) - exc = tw.stringio.getvalue() + exc = file.getvalue() return exc.strip() @property diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 412f11edc05..f0c7146c7e5 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,3 +1,4 @@ +import io import operator import os import queue @@ -1037,10 +1038,11 @@ def f(): """ ) excinfo = pytest.raises(ValueError, mod.f) - tw = TerminalWriter(stringio=True) + file = io.StringIO() + tw = TerminalWriter(file=file) repr = excinfo.getrepr(**reproptions) repr.toterminal(tw) - assert tw.stringio.getvalue() + assert file.getvalue() def test_traceback_repr_style(self, importasmod, tw_mock): mod = importasmod( @@ -1255,11 +1257,12 @@ def g(): getattr(excinfo.value, attr).__traceback__ = None r = excinfo.getrepr() - tw = TerminalWriter(stringio=True) + file = io.StringIO() + tw = TerminalWriter(file=file) tw.hasmarkup = False r.toterminal(tw) - matcher = LineMatcher(tw.stringio.getvalue().splitlines()) + matcher = LineMatcher(file.getvalue().splitlines()) matcher.fnmatch_lines( [ "ValueError: invalid value", From 8d2d1c40f8030b337dedeabcb4a1e54fb5316176 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:50:14 +0300 Subject: [PATCH 132/823] terminalwriter: inline function _escaped Doesn't add much. --- src/_pytest/_io/terminalwriter.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 907b905436d..d86b1aef7e2 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -118,11 +118,6 @@ def width_of_current_line(self): """ return self._width_of_current_line - def _escaped(self, text, esc): - if esc and self.hasmarkup: - text = "".join(["\x1b[%sm" % cod for cod in esc]) + text + "\x1b[0m" - return text - def markup(self, text, **kw): esc = [] for name in kw: @@ -130,7 +125,9 @@ def markup(self, text, **kw): raise ValueError("unknown markup: {!r}".format(name)) if kw[name]: esc.append(self._esctable[name]) - return self._escaped(text, tuple(esc)) + if esc and self.hasmarkup: + text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m" + return text def sep(self, sepchar, title=None, fullwidth=None, **kw): if fullwidth is None: From f6564a548a6587722be6d4122d96d9136ff0f4ee Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 16:55:08 +0300 Subject: [PATCH 133/823] terminalwriter: remove win32 specific code in favor of relying on colorama On Windows we already depend on colorama, which takes care of all of this custom code on its own. --- src/_pytest/_io/terminalwriter.py | 123 ++---------------------------- 1 file changed, 7 insertions(+), 116 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index d86b1aef7e2..2e213e93a8e 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -10,20 +10,6 @@ # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. -win32_and_ctypes = False -colorama = None -if sys.platform == "win32": - try: - import colorama - except ImportError: - try: - import ctypes - except ImportError: - pass - else: - win32_and_ctypes = True - - def get_terminal_width() -> int: width, _ = shutil.get_terminal_size(fallback=(80, 24)) @@ -85,8 +71,13 @@ class TerminalWriter: def __init__(self, file=None): if file is None: file = sys.stdout - if hasattr(file, "isatty") and file.isatty() and colorama: - file = colorama.AnsiToWin32(file).stream + if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32": + try: + import colorama + except ImportError: + pass + else: + file = colorama.AnsiToWin32(file).stream self._file = file self.hasmarkup = should_do_markup(file) self._chars_on_current_line = 0 @@ -216,103 +207,3 @@ def _highlight(self, source): return source else: return highlight(source, PythonLexer(), TerminalFormatter(bg="dark")) - - -if win32_and_ctypes: - import ctypes # noqa: F811 - from ctypes import wintypes - from ctypes import windll # type: ignore[attr-defined] # noqa: F821 - - class Win32ConsoleWriter(TerminalWriter): - def write(self, msg: str, **kw): - if msg: - self._update_chars_on_current_line(msg) - - oldcolors = None - if self.hasmarkup and kw: - handle = GetStdHandle(STD_OUTPUT_HANDLE) - oldcolors = GetConsoleInfo(handle).wAttributes - default_bg = oldcolors & 0x00F0 - attr = default_bg - if kw.pop("bold", False): - attr |= FOREGROUND_INTENSITY - - if kw.pop("red", False): - attr |= FOREGROUND_RED - elif kw.pop("blue", False): - attr |= FOREGROUND_BLUE - elif kw.pop("green", False): - attr |= FOREGROUND_GREEN - elif kw.pop("yellow", False): - attr |= FOREGROUND_GREEN | FOREGROUND_RED - else: - attr |= oldcolors & 0x0007 - - SetConsoleTextAttribute(handle, attr) - self._file.write(msg) - self._file.flush() - if oldcolors: - SetConsoleTextAttribute(handle, oldcolors) - - TerminalWriter = Win32ConsoleWriter # type: ignore[misc] # noqa: F821 - - # ctypes access to the Windows console - STD_OUTPUT_HANDLE = -11 - STD_ERROR_HANDLE = -12 - FOREGROUND_BLACK = 0x0000 # black text - FOREGROUND_BLUE = 0x0001 # text color contains blue. - FOREGROUND_GREEN = 0x0002 # text color contains green. - FOREGROUND_RED = 0x0004 # text color contains red. - FOREGROUND_WHITE = 0x0007 - FOREGROUND_INTENSITY = 0x0008 # text color is intensified. - BACKGROUND_BLACK = 0x0000 # background color black - BACKGROUND_BLUE = 0x0010 # background color contains blue. - BACKGROUND_GREEN = 0x0020 # background color contains green. - BACKGROUND_RED = 0x0040 # background color contains red. - BACKGROUND_WHITE = 0x0070 - BACKGROUND_INTENSITY = 0x0080 # background color is intensified. - - SHORT = ctypes.c_short - - class COORD(ctypes.Structure): - _fields_ = [("X", SHORT), ("Y", SHORT)] - - class SMALL_RECT(ctypes.Structure): - _fields_ = [ - ("Left", SHORT), - ("Top", SHORT), - ("Right", SHORT), - ("Bottom", SHORT), - ] - - class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): - _fields_ = [ - ("dwSize", COORD), - ("dwCursorPosition", COORD), - ("wAttributes", wintypes.WORD), - ("srWindow", SMALL_RECT), - ("dwMaximumWindowSize", COORD), - ] - - _GetStdHandle = windll.kernel32.GetStdHandle - _GetStdHandle.argtypes = [wintypes.DWORD] - _GetStdHandle.restype = wintypes.HANDLE - - def GetStdHandle(kind): - return _GetStdHandle(kind) - - SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute - SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD] - SetConsoleTextAttribute.restype = wintypes.BOOL - - _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo - _GetConsoleScreenBufferInfo.argtypes = [ - wintypes.HANDLE, - ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), - ] - _GetConsoleScreenBufferInfo.restype = wintypes.BOOL - - def GetConsoleInfo(handle): - info = CONSOLE_SCREEN_BUFFER_INFO() - _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) - return info From e8fc5f99fa1a0b258da3713674a1a99948441eeb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 17:08:18 +0300 Subject: [PATCH 134/823] terminalwriter: add type annotations --- src/_pytest/_io/terminalwriter.py | 50 +++++++++++++++++-------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 2e213e93a8e..0ab6a31da9c 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -4,7 +4,9 @@ import sys import unicodedata from functools import lru_cache +from typing import Optional from typing import Sequence +from typing import TextIO # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. @@ -26,12 +28,12 @@ def char_width(c: str) -> int: return 2 if unicodedata.east_asian_width(c) in ("F", "W") else 1 -def get_line_width(text): +def get_line_width(text: str) -> int: text = unicodedata.normalize("NFC", text) return sum(char_width(c) for c in text) -def should_do_markup(file): +def should_do_markup(file: TextIO) -> bool: if os.environ.get("PY_COLORS") == "1": return True if os.environ.get("PY_COLORS") == "0": @@ -68,7 +70,7 @@ class TerminalWriter: invert=7, ) - def __init__(self, file=None): + def __init__(self, file: Optional[TextIO] = None) -> None: if file is None: file = sys.stdout if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32": @@ -78,38 +80,33 @@ def __init__(self, file=None): pass else: file = colorama.AnsiToWin32(file).stream + assert file is not None self._file = file self.hasmarkup = should_do_markup(file) self._chars_on_current_line = 0 self._width_of_current_line = 0 @property - def fullwidth(self): + def fullwidth(self) -> int: if hasattr(self, "_terminal_width"): return self._terminal_width return get_terminal_width() @fullwidth.setter - def fullwidth(self, value): + def fullwidth(self, value: int) -> None: self._terminal_width = value @property - def chars_on_current_line(self): - """Return the number of characters written so far in the current line. - - :rtype: int - """ + def chars_on_current_line(self) -> int: + """Return the number of characters written so far in the current line.""" return self._chars_on_current_line @property - def width_of_current_line(self): - """Return an estimate of the width so far in the current line. - - :rtype: int - """ + def width_of_current_line(self) -> int: + """Return an estimate of the width so far in the current line.""" return self._width_of_current_line - def markup(self, text, **kw): + def markup(self, text: str, **kw: bool) -> str: esc = [] for name in kw: if name not in self._esctable: @@ -120,7 +117,13 @@ def markup(self, text, **kw): text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m" return text - def sep(self, sepchar, title=None, fullwidth=None, **kw): + def sep( + self, + sepchar: str, + title: Optional[str] = None, + fullwidth: Optional[int] = None, + **kw: bool + ) -> None: if fullwidth is None: fullwidth = self.fullwidth # the goal is to have the line be as long as possible @@ -151,7 +154,7 @@ def sep(self, sepchar, title=None, fullwidth=None, **kw): self.line(line, **kw) - def write(self, msg: str, **kw) -> None: + def write(self, msg: str, **kw: bool) -> None: if msg: self._update_chars_on_current_line(msg) @@ -171,7 +174,7 @@ def _update_chars_on_current_line(self, text: str) -> None: self._chars_on_current_line += len(current_line) self._width_of_current_line += get_line_width(current_line) - def line(self, s: str = "", **kw): + def line(self, s: str = "", **kw: bool) -> None: self.write(s, **kw) self.write("\n") @@ -195,8 +198,8 @@ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> No for indent, new_line in zip(indents, new_lines): self.line(indent + new_line) - def _highlight(self, source): - """Highlight the given source code if we have markup support""" + def _highlight(self, source: str) -> str: + """Highlight the given source code if we have markup support.""" if not self.hasmarkup: return source try: @@ -206,4 +209,7 @@ def _highlight(self, source): except ImportError: return source else: - return highlight(source, PythonLexer(), TerminalFormatter(bg="dark")) + highlighted = highlight( + source, PythonLexer(), TerminalFormatter(bg="dark") + ) # type: str + return highlighted From 8e04d35a3347a5e5b1152047d52e10032cfb509f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 17:09:13 +0300 Subject: [PATCH 135/823] terminalwriter: remove unneeded hasattr use --- src/_pytest/_io/terminalwriter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 0ab6a31da9c..a4989a7f0a8 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -85,10 +85,11 @@ def __init__(self, file: Optional[TextIO] = None) -> None: self.hasmarkup = should_do_markup(file) self._chars_on_current_line = 0 self._width_of_current_line = 0 + self._terminal_width = None # type: Optional[int] @property def fullwidth(self) -> int: - if hasattr(self, "_terminal_width"): + if self._terminal_width is not None: return self._terminal_width return get_terminal_width() From d9b43647b792ed71d92946edee17ded5d86eb6dc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 17:38:16 +0300 Subject: [PATCH 136/823] terminalwriter: inline function _update_chars_on_current_line --- src/_pytest/_io/terminalwriter.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index a4989a7f0a8..5db4dc8b6d7 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -157,7 +157,13 @@ def sep( def write(self, msg: str, **kw: bool) -> None: if msg: - self._update_chars_on_current_line(msg) + current_line = msg.rsplit("\n", 1)[-1] + if "\n" in msg: + self._chars_on_current_line = len(current_line) + self._width_of_current_line = get_line_width(current_line) + else: + self._chars_on_current_line += len(current_line) + self._width_of_current_line += get_line_width(current_line) if self.hasmarkup and kw: markupmsg = self.markup(msg, **kw) @@ -166,15 +172,6 @@ def write(self, msg: str, **kw: bool) -> None: self._file.write(markupmsg) self._file.flush() - def _update_chars_on_current_line(self, text: str) -> None: - current_line = text.rsplit("\n", 1)[-1] - if "\n" in text: - self._chars_on_current_line = len(current_line) - self._width_of_current_line = get_line_width(current_line) - else: - self._chars_on_current_line += len(current_line) - self._width_of_current_line += get_line_width(current_line) - def line(self, s: str = "", **kw: bool) -> None: self.write(s, **kw) self.write("\n") From 1bc4170e63ca430a4f5e8532a53a71a060d115fa Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 17:58:33 +0300 Subject: [PATCH 137/823] terminalwriter: don't flush implicitly; add explicit flushes Flushing on every write is somewhat expensive. Rely on line buffering instead (if line buffering for stdout is disabled, there must be some reason...), and add explicit flushes when not outputting lines. This is how regular `print()` e.g. work so should be familiar. --- src/_pytest/_io/terminalwriter.py | 8 ++++++-- src/_pytest/python.py | 2 +- src/_pytest/runner.py | 1 + src/_pytest/setuponly.py | 2 ++ src/_pytest/terminal.py | 18 ++++++++++++------ 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 5db4dc8b6d7..7bd8507c2cb 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -155,7 +155,7 @@ def sep( self.line(line, **kw) - def write(self, msg: str, **kw: bool) -> None: + def write(self, msg: str, *, flush: bool = False, **kw: bool) -> None: if msg: current_line = msg.rsplit("\n", 1)[-1] if "\n" in msg: @@ -170,12 +170,16 @@ def write(self, msg: str, **kw: bool) -> None: else: markupmsg = msg self._file.write(markupmsg) - self._file.flush() + if flush: + self.flush() def line(self, s: str = "", **kw: bool) -> None: self.write(s, **kw) self.write("\n") + def flush(self) -> None: + self._file.flush() + def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None: """Write lines of source code possibly highlighted. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2b9bf4f5bb5..f472354efe5 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1424,7 +1424,7 @@ def _showfixtures_main(config, session): def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: for line in doc.split("\n"): - tw.write(indent + line + "\n") + tw.line(indent + line) class Function(PyobjMixin, nodes.Item): diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index f87ccb461ed..76785ada7f3 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -120,6 +120,7 @@ def show_test_item(item): used_fixtures = sorted(getattr(item, "fixturenames", [])) if used_fixtures: tw.write(" (fixtures used: {})".format(", ".join(used_fixtures))) + tw.flush() def pytest_runtest_setup(item): diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index aa5a95ff920..c9cc589ffee 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -68,6 +68,8 @@ def _show_fixture_action(fixturedef, msg): if hasattr(fixturedef, "cached_param"): tw.write("[{}]".format(fixturedef.cached_param)) + tw.flush() + if capman: capman.resume_global_capture() diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 52c04a49c3b..d4645673328 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -343,7 +343,7 @@ def write_fspath_result(self, nodeid, res, **markup): fspath = self.startdir.bestrelpath(fspath) self._tw.line() self._tw.write(fspath + " ") - self._tw.write(res, **markup) + self._tw.write(res, flush=True, **markup) def write_ensure_prefix(self, prefix, extra="", **kwargs): if self.currentfspath != prefix: @@ -359,8 +359,11 @@ def ensure_newline(self): self._tw.line() self.currentfspath = None - def write(self, content, **markup): - self._tw.write(content, **markup) + def write(self, content: str, *, flush: bool = False, **markup: bool) -> None: + self._tw.write(content, flush=flush, **markup) + + def flush(self) -> None: + self._tw.flush() def write_line(self, line, **markup): if not isinstance(line, str): @@ -437,9 +440,11 @@ def pytest_runtest_logstart(self, nodeid, location): if self.showlongtestinfo: line = self._locationline(nodeid, *location) self.write_ensure_prefix(line, "") + self.flush() elif self.showfspath: fsid = nodeid.split("::")[0] self.write_fspath_result(fsid, "") + self.flush() def pytest_runtest_logreport(self, report: TestReport) -> None: self._tests_ran = True @@ -491,6 +496,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: self._tw.write(word, **markup) self._tw.write(" " + line) self.currentfspath = -2 + self.flush() @property def _is_last_item(self): @@ -539,7 +545,7 @@ def _write_progress_information_filling_space(self): msg = self._get_progress_information_message() w = self._width_of_current_line fill = self._tw.fullwidth - w - 1 - self.write(msg.rjust(fill), **{color: True}) + self.write(msg.rjust(fill), flush=True, **{color: True}) @property def _width_of_current_line(self): @@ -553,10 +559,10 @@ def _width_of_current_line(self): def pytest_collection(self) -> None: if self.isatty: if self.config.option.verbose >= 0: - self.write("collecting ... ", bold=True) + self.write("collecting ... ", flush=True, bold=True) self._collect_report_last_write = time.time() elif self.config.option.verbose >= 1: - self.write("collecting ... ", bold=True) + self.write("collecting ... ", flush=True, bold=True) def pytest_collectreport(self, report: CollectReport) -> None: if report.failed: From dd32c72ff0a9b875e3efbd31e28d63feaac8f32d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 18:10:54 +0300 Subject: [PATCH 138/823] terminalwriter: remove unused property chars_on_current_line --- src/_pytest/_io/terminalwriter.py | 8 -------- src/_pytest/terminal.py | 6 +----- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 7bd8507c2cb..124ffe79532 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -83,7 +83,6 @@ def __init__(self, file: Optional[TextIO] = None) -> None: assert file is not None self._file = file self.hasmarkup = should_do_markup(file) - self._chars_on_current_line = 0 self._width_of_current_line = 0 self._terminal_width = None # type: Optional[int] @@ -97,11 +96,6 @@ def fullwidth(self) -> int: def fullwidth(self, value: int) -> None: self._terminal_width = value - @property - def chars_on_current_line(self) -> int: - """Return the number of characters written so far in the current line.""" - return self._chars_on_current_line - @property def width_of_current_line(self) -> int: """Return an estimate of the width so far in the current line.""" @@ -159,10 +153,8 @@ def write(self, msg: str, *, flush: bool = False, **kw: bool) -> None: if msg: current_line = msg.rsplit("\n", 1)[-1] if "\n" in msg: - self._chars_on_current_line = len(current_line) self._width_of_current_line = get_line_width(current_line) else: - self._chars_on_current_line += len(current_line) self._width_of_current_line += get_line_width(current_line) if self.hasmarkup and kw: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index d4645673328..39deaca559f 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -550,11 +550,7 @@ def _write_progress_information_filling_space(self): @property def _width_of_current_line(self): """Return the width of current line, using the superior implementation of py-1.6 when available""" - try: - return self._tw.width_of_current_line - except AttributeError: - # py < 1.6.0 - return self._tw.chars_on_current_line + return self._tw.width_of_current_line def pytest_collection(self) -> None: if self.isatty: From d5584c7207e094aa833358285f749e01f907aa00 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 18:14:57 +0300 Subject: [PATCH 139/823] terminalwriter: compute width_of_current_line lazily Currently this property is computed eagerly, which means get_line_width() is computed on everything written, but that is a slow function. Compute it lazily, so that get_line_width() only runs when needed. --- src/_pytest/_io/terminalwriter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 124ffe79532..204222c88e0 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -83,7 +83,7 @@ def __init__(self, file: Optional[TextIO] = None) -> None: assert file is not None self._file = file self.hasmarkup = should_do_markup(file) - self._width_of_current_line = 0 + self._current_line = "" self._terminal_width = None # type: Optional[int] @property @@ -99,7 +99,7 @@ def fullwidth(self, value: int) -> None: @property def width_of_current_line(self) -> int: """Return an estimate of the width so far in the current line.""" - return self._width_of_current_line + return get_line_width(self._current_line) def markup(self, text: str, **kw: bool) -> str: esc = [] @@ -153,9 +153,9 @@ def write(self, msg: str, *, flush: bool = False, **kw: bool) -> None: if msg: current_line = msg.rsplit("\n", 1)[-1] if "\n" in msg: - self._width_of_current_line = get_line_width(current_line) + self._current_line = current_line else: - self._width_of_current_line += get_line_width(current_line) + self._current_line += current_line if self.hasmarkup and kw: markupmsg = self.markup(msg, **kw) From 0e36596268e33f55ef9e491e1f84301536c7e41a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 19:10:45 +0300 Subject: [PATCH 140/823] testing/io: port TerminalWriter tests from py --- testing/io/test_terminalwriter.py | 209 ++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 testing/io/test_terminalwriter.py diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py new file mode 100644 index 00000000000..1e4f04ac0d0 --- /dev/null +++ b/testing/io/test_terminalwriter.py @@ -0,0 +1,209 @@ +import io +import os +import shutil +import sys +from typing import Generator +from unittest import mock + +import pytest +from _pytest._io import terminalwriter +from _pytest.monkeypatch import MonkeyPatch + + +# These tests were initially copied from py 1.8.1. + + +def test_terminal_width_COLUMNS(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setenv("COLUMNS", "42") + assert terminalwriter.get_terminal_width() == 42 + monkeypatch.delenv("COLUMNS", raising=False) + + +def test_terminalwriter_width_bogus(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr(shutil, "get_terminal_size", mock.Mock(return_value=(10, 10))) + monkeypatch.delenv("COLUMNS", raising=False) + tw = terminalwriter.TerminalWriter() + assert tw.fullwidth == 80 + + +def test_terminalwriter_computes_width(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr(terminalwriter, "get_terminal_width", lambda: 42) + tw = terminalwriter.TerminalWriter() + assert tw.fullwidth == 42 + + +def test_terminalwriter_dumb_term_no_markup(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr(os, "environ", {"TERM": "dumb", "PATH": ""}) + + class MyFile: + closed = False + + def isatty(self): + return True + + with monkeypatch.context() as m: + m.setattr(sys, "stdout", MyFile()) + assert sys.stdout.isatty() + tw = terminalwriter.TerminalWriter() + assert not tw.hasmarkup + + +win32 = int(sys.platform == "win32") + + +class TestTerminalWriter: + @pytest.fixture(params=["path", "stringio"]) + def tw( + self, request, tmpdir + ) -> Generator[terminalwriter.TerminalWriter, None, None]: + if request.param == "path": + p = tmpdir.join("tmpfile") + f = open(str(p), "w+", encoding="utf8") + tw = terminalwriter.TerminalWriter(f) + + def getlines(): + f.flush() + with open(str(p), encoding="utf8") as fp: + return fp.readlines() + + elif request.param == "stringio": + f = io.StringIO() + tw = terminalwriter.TerminalWriter(f) + + def getlines(): + f.seek(0) + return f.readlines() + + tw.getlines = getlines # type: ignore + tw.getvalue = lambda: "".join(getlines()) # type: ignore + + with f: + yield tw + + def test_line(self, tw) -> None: + tw.line("hello") + lines = tw.getlines() + assert len(lines) == 1 + assert lines[0] == "hello\n" + + def test_line_unicode(self, tw) -> None: + msg = "b\u00f6y" + tw.line(msg) + lines = tw.getlines() + assert lines[0] == msg + "\n" + + def test_sep_no_title(self, tw) -> None: + tw.sep("-", fullwidth=60) + lines = tw.getlines() + assert len(lines) == 1 + assert lines[0] == "-" * (60 - win32) + "\n" + + def test_sep_with_title(self, tw) -> None: + tw.sep("-", "hello", fullwidth=60) + lines = tw.getlines() + assert len(lines) == 1 + assert lines[0] == "-" * 26 + " hello " + "-" * (27 - win32) + "\n" + + def test_sep_longer_than_width(self, tw) -> None: + tw.sep("-", "a" * 10, fullwidth=5) + (line,) = tw.getlines() + # even though the string is wider than the line, still have a separator + assert line == "- aaaaaaaaaa -\n" + + @pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi") + def test_markup(self, tw) -> None: + for bold in (True, False): + for color in ("red", "green"): + text2 = tw.markup("hello", **{color: True, "bold": bold}) + assert text2.find("hello") != -1 + with pytest.raises(ValueError): + tw.markup("x", wronkw=3) + with pytest.raises(ValueError): + tw.markup("x", wronkw=0) + + def test_line_write_markup(self, tw) -> None: + tw.hasmarkup = True + tw.line("x", bold=True) + tw.write("x\n", red=True) + lines = tw.getlines() + if sys.platform != "win32": + assert len(lines[0]) >= 2, lines + assert len(lines[1]) >= 2, lines + + def test_attr_fullwidth(self, tw) -> None: + tw.sep("-", "hello", fullwidth=70) + tw.fullwidth = 70 + tw.sep("-", "hello") + lines = tw.getlines() + assert len(lines[0]) == len(lines[1]) + + +@pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi") +def test_attr_hasmarkup() -> None: + file = io.StringIO() + tw = terminalwriter.TerminalWriter(file) + assert not tw.hasmarkup + tw.hasmarkup = True + tw.line("hello", bold=True) + s = file.getvalue() + assert len(s) > len("hello\n") + assert "\x1b[1m" in s + assert "\x1b[0m" in s + + +def test_should_do_markup_PY_COLORS_eq_1(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setitem(os.environ, "PY_COLORS", "1") + file = io.StringIO() + tw = terminalwriter.TerminalWriter(file) + assert tw.hasmarkup + tw.line("hello", bold=True) + s = file.getvalue() + assert len(s) > len("hello\n") + assert "\x1b[1m" in s + assert "\x1b[0m" in s + + +def test_should_do_markup_PY_COLORS_eq_0(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setitem(os.environ, "PY_COLORS", "0") + f = io.StringIO() + f.isatty = lambda: True # type: ignore + tw = terminalwriter.TerminalWriter(file=f) + assert not tw.hasmarkup + tw.line("hello", bold=True) + s = f.getvalue() + assert s == "hello\n" + + +class TestTerminalWriterLineWidth: + def test_init(self) -> None: + tw = terminalwriter.TerminalWriter() + assert tw.width_of_current_line == 0 + + def test_update(self) -> None: + tw = terminalwriter.TerminalWriter() + tw.write("hello world") + assert tw.width_of_current_line == 11 + + def test_update_with_newline(self) -> None: + tw = terminalwriter.TerminalWriter() + tw.write("hello\nworld") + assert tw.width_of_current_line == 5 + + def test_update_with_wide_text(self) -> None: + tw = terminalwriter.TerminalWriter() + tw.write("乇乂ㄒ尺卂 ㄒ卄丨匚匚") + assert tw.width_of_current_line == 21 # 5*2 + 1 + 5*2 + + def test_composed(self) -> None: + tw = terminalwriter.TerminalWriter() + text = "café food" + assert len(text) == 9 + tw.write(text) + assert tw.width_of_current_line == 9 + + def test_combining(self) -> None: + tw = terminalwriter.TerminalWriter() + text = "café food" + assert len(text) == 10 + tw.write(text) + assert tw.width_of_current_line == 9 From bafc9bd58b0cd244ba23da51cea01a792c1ea641 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 14:47:34 +0300 Subject: [PATCH 141/823] testing: merge code/test_terminal_writer.py into io/test_terminalwriter.py --- testing/code/test_terminal_writer.py | 28 ---------------------------- testing/io/test_terminalwriter.py | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 28 deletions(-) delete mode 100644 testing/code/test_terminal_writer.py diff --git a/testing/code/test_terminal_writer.py b/testing/code/test_terminal_writer.py deleted file mode 100644 index 01da3c23500..00000000000 --- a/testing/code/test_terminal_writer.py +++ /dev/null @@ -1,28 +0,0 @@ -import re -from io import StringIO - -import pytest -from _pytest._io import TerminalWriter - - -@pytest.mark.parametrize( - "has_markup, expected", - [ - pytest.param( - True, "{kw}assert{hl-reset} {number}0{hl-reset}\n", id="with markup" - ), - pytest.param(False, "assert 0\n", id="no markup"), - ], -) -def test_code_highlight(has_markup, expected, color_mapping): - f = StringIO() - tw = TerminalWriter(f) - tw.hasmarkup = has_markup - tw._write_source(["assert 0"]) - assert f.getvalue().splitlines(keepends=True) == color_mapping.format([expected]) - - with pytest.raises( - ValueError, - match=re.escape("indents size (2) should have same size as lines (1)"), - ): - tw._write_source(["assert 0"], [" ", " "]) diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py index 1e4f04ac0d0..b3bd9cfaeb1 100644 --- a/testing/io/test_terminalwriter.py +++ b/testing/io/test_terminalwriter.py @@ -1,5 +1,6 @@ import io import os +import re import shutil import sys from typing import Generator @@ -207,3 +208,26 @@ def test_combining(self) -> None: assert len(text) == 10 tw.write(text) assert tw.width_of_current_line == 9 + + +@pytest.mark.parametrize( + "has_markup, expected", + [ + pytest.param( + True, "{kw}assert{hl-reset} {number}0{hl-reset}\n", id="with markup" + ), + pytest.param(False, "assert 0\n", id="no markup"), + ], +) +def test_code_highlight(has_markup, expected, color_mapping): + f = io.StringIO() + tw = terminalwriter.TerminalWriter(f) + tw.hasmarkup = has_markup + tw._write_source(["assert 0"]) + assert f.getvalue().splitlines(keepends=True) == color_mapping.format([expected]) + + with pytest.raises( + ValueError, + match=re.escape("indents size (2) should have same size as lines (1)"), + ): + tw._write_source(["assert 0"], [" ", " "]) From 414a87a53f1b424c5e5073583e6cd978857a1d9b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 14:20:48 +0300 Subject: [PATCH 142/823] config/argparsing: use our own get_terminal_width() --- src/_pytest/_io/__init__.py | 2 ++ src/_pytest/config/argparsing.py | 3 ++- testing/test_config.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index 880c3c87a9f..db001e918cb 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -1,6 +1,8 @@ +from .terminalwriter import get_terminal_width from .terminalwriter import TerminalWriter __all__ = [ "TerminalWriter", + "get_terminal_width", ] diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 140e04e9723..940eaa6a799 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -15,6 +15,7 @@ import py +import _pytest._io from _pytest.compat import TYPE_CHECKING from _pytest.config.exceptions import UsageError @@ -466,7 +467,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): def __init__(self, *args: Any, **kwargs: Any) -> None: """Use more accurate terminal width via pylib.""" if "width" not in kwargs: - kwargs["width"] = py.io.get_terminal_width() + kwargs["width"] = _pytest._io.get_terminal_width() super().__init__(*args, **kwargs) def _format_action_invocation(self, action: argparse.Action) -> str: diff --git a/testing/test_config.py b/testing/test_config.py index 9035407b76b..0c05c4fad7e 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1253,7 +1253,7 @@ def test_help_formatter_uses_py_get_terminal_width(monkeypatch): formatter = DropShorterLongHelpFormatter("prog") assert formatter._width == 90 - monkeypatch.setattr("py.io.get_terminal_width", lambda: 160) + monkeypatch.setattr("_pytest._io.get_terminal_width", lambda: 160) formatter = DropShorterLongHelpFormatter("prog") assert formatter._width == 160 From d8558e87c596b9ff7bbac829689f149a2030e33f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 14:40:59 +0300 Subject: [PATCH 143/823] terminalwriter: clean up markup function a bit --- src/_pytest/_io/terminalwriter.py | 31 ++++++++++++++----------------- testing/io/test_terminalwriter.py | 12 +++++++----- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 204222c88e0..4f22f5a7ad7 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -101,15 +101,14 @@ def width_of_current_line(self) -> int: """Return an estimate of the width so far in the current line.""" return get_line_width(self._current_line) - def markup(self, text: str, **kw: bool) -> str: - esc = [] - for name in kw: + def markup(self, text: str, **markup: bool) -> str: + for name in markup: if name not in self._esctable: raise ValueError("unknown markup: {!r}".format(name)) - if kw[name]: - esc.append(self._esctable[name]) - if esc and self.hasmarkup: - text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m" + if self.hasmarkup: + esc = [self._esctable[name] for name, on in markup.items() if on] + if esc: + text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m" return text def sep( @@ -117,7 +116,7 @@ def sep( sepchar: str, title: Optional[str] = None, fullwidth: Optional[int] = None, - **kw: bool + **markup: bool ) -> None: if fullwidth is None: fullwidth = self.fullwidth @@ -147,9 +146,9 @@ def sep( if len(line) + len(sepchar.rstrip()) <= fullwidth: line += sepchar.rstrip() - self.line(line, **kw) + self.line(line, **markup) - def write(self, msg: str, *, flush: bool = False, **kw: bool) -> None: + def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None: if msg: current_line = msg.rsplit("\n", 1)[-1] if "\n" in msg: @@ -157,16 +156,14 @@ def write(self, msg: str, *, flush: bool = False, **kw: bool) -> None: else: self._current_line += current_line - if self.hasmarkup and kw: - markupmsg = self.markup(msg, **kw) - else: - markupmsg = msg - self._file.write(markupmsg) + msg = self.markup(msg, **markup) + + self._file.write(msg) if flush: self.flush() - def line(self, s: str = "", **kw: bool) -> None: - self.write(s, **kw) + def line(self, s: str = "", **markup: bool) -> None: + self.write(s, **markup) self.write("\n") def flush(self) -> None: diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py index b3bd9cfaeb1..0e9cdb64d06 100644 --- a/testing/io/test_terminalwriter.py +++ b/testing/io/test_terminalwriter.py @@ -112,11 +112,13 @@ def test_sep_longer_than_width(self, tw) -> None: assert line == "- aaaaaaaaaa -\n" @pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi") - def test_markup(self, tw) -> None: - for bold in (True, False): - for color in ("red", "green"): - text2 = tw.markup("hello", **{color: True, "bold": bold}) - assert text2.find("hello") != -1 + @pytest.mark.parametrize("bold", (True, False)) + @pytest.mark.parametrize("color", ("red", "green")) + def test_markup(self, tw, bold: bool, color: str) -> None: + text = tw.markup("hello", **{color: True, "bold": bold}) + assert "hello" in text + + def test_markup_bad(self, tw) -> None: with pytest.raises(ValueError): tw.markup("x", wronkw=3) with pytest.raises(ValueError): From e40bf1d1da6771f951bd4b6126fc3cb107a7c9e7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 16:40:03 +0300 Subject: [PATCH 144/823] Add a changelog for TerminalWriter changes --- changelog/7135.breaking.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 changelog/7135.breaking.rst diff --git a/changelog/7135.breaking.rst b/changelog/7135.breaking.rst new file mode 100644 index 00000000000..4d5df4e402a --- /dev/null +++ b/changelog/7135.breaking.rst @@ -0,0 +1,15 @@ +Pytest now uses its own ``TerminalWriter`` class instead of using the one from the ``py`` library. +Plugins generally access this class through ``TerminalReporter.writer``, ``TerminalReporter.write()`` +(and similar methods), or ``_pytest.config.create_terminal_writer()``. + +The following breaking changes were made: + +- Output (``write()`` method and others) no longer flush implicitly; the flushing behavior + of the underlying file is respected. To flush explicitly (for example, if you + want output to be shown before an end-of-line is printed), use ``write(flush=True)`` or + ``terminal_writer.flush()``. +- Explicit Windows console support was removed, delegated to the colorama library. +- Support for writing ``bytes`` was removed. +- The ``reline`` method and ``chars_on_current_line`` property were removed. +- The ``stringio`` and ``encoding`` arguments was removed. +- Support for passing a callable instead of a file was removed. From 409ffcef177365d7de54df8a2b114b0c6cee5687 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 11:39:38 +0300 Subject: [PATCH 145/823] Remove a couple Python 2 __nonzero__ definitions It's called __bool__ in Python 3. --- src/_pytest/mark/evaluate.py | 2 -- testing/test_assertrewrite.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_pytest/mark/evaluate.py b/src/_pytest/mark/evaluate.py index 772baf31b6e..c47174e7156 100644 --- a/src/_pytest/mark/evaluate.py +++ b/src/_pytest/mark/evaluate.py @@ -38,8 +38,6 @@ def __bool__(self): # don't cache here to prevent staleness return bool(self._get_marks()) - __nonzero__ = __bool__ - def wasvalid(self): return not hasattr(self, "exc") diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 8cf4929662e..726b6bdcc09 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -640,10 +640,10 @@ def f(): assert getmsg(f) == "assert 5 <= 4" - def test_assert_raising_nonzero_in_comparison(self): + def test_assert_raising__bool__in_comparison(self): def f(): class A: - def __nonzero__(self): + def __bool__(self): raise ValueError(42) def __lt__(self, other): From a718ad636304ab594eb95c8c64ac28a96354757d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 25 Apr 2020 13:38:53 +0300 Subject: [PATCH 146/823] Stop using Python's eval() for -m and -k Previously, the expressions given to the `-m` and `-k` options were evaluated with `eval`. This causes a few issues: - Python keywords cannot be used. - Constants like numbers, None, True, False are not handled correctly. - Various syntax like numeric operators and `X if Y else Z` is supported unintentionally. - `eval()` is somewhat dangerous for arbitrary input. - Can fail in many ways so requires `except Exception`. The format we want to support is quite simple, so change to a custom parser. This fixes the issues above, and gives us full control of the format, so can be documented comprehensively and even be extended in the future if we wish. --- changelog/7122.breaking.rst | 3 + doc/en/example/markers.rst | 18 +--- src/_pytest/mark/expression.py | 173 ++++++++++++++++++++++++++++++++ src/_pytest/mark/legacy.py | 65 +++++------- testing/test_mark.py | 59 +++++++---- testing/test_mark_expression.py | 162 ++++++++++++++++++++++++++++++ 6 files changed, 405 insertions(+), 75 deletions(-) create mode 100644 changelog/7122.breaking.rst create mode 100644 src/_pytest/mark/expression.py create mode 100644 testing/test_mark_expression.py diff --git a/changelog/7122.breaking.rst b/changelog/7122.breaking.rst new file mode 100644 index 00000000000..7fe329c9ff6 --- /dev/null +++ b/changelog/7122.breaking.rst @@ -0,0 +1,3 @@ +Expressions given to the ``-m`` and ``-k`` options are no longer evaluated using Python's ``eval()``. +The format supports ``or``, ``and``, ``not``, parenthesis and general identifiers to match against. +Python constants, keywords or other operators are no longer evaluated differently. diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 467c2a2faf8..e791f489d0d 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -141,14 +141,14 @@ Or select multiple nodes: Using ``-k expr`` to select tests based on their name ------------------------------------------------------- -.. versionadded: 2.0/2.3.4 +.. versionadded:: 2.0/2.3.4 You can use the ``-k`` command line option to specify an expression which implements a substring match on the test names instead of the exact match on markers that ``-m`` provides. This makes it easy to select tests based on their names: -.. versionadded: 5.4 +.. versionchanged:: 5.4 The expression matching is now case-insensitive. @@ -198,20 +198,8 @@ Or to select "http" and "quick" tests: ===================== 2 passed, 2 deselected in 0.12s ====================== -.. note:: - - If you are using expressions such as ``"X and Y"`` then both ``X`` and ``Y`` - need to be simple non-keyword names. For example, ``"pass"`` or ``"from"`` - will result in SyntaxErrors because ``"-k"`` evaluates the expression using - Python's `eval`_ function. - -.. _`eval`: https://docs.python.org/3.6/library/functions.html#eval - +You can use ``and``, ``or``, ``not`` and parentheses. - However, if the ``"-k"`` argument is a simple string, no such restrictions - apply. Also ``"-k 'not STRING'"`` has no restrictions. You can also - specify numbers like ``"-k 1.3"`` to match tests which are parametrized - with the float ``"1.3"``. Registering markers ------------------------------------- diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py new file mode 100644 index 00000000000..008192d4af2 --- /dev/null +++ b/src/_pytest/mark/expression.py @@ -0,0 +1,173 @@ +r""" +Evaluate match expressions, as used by `-k` and `-m`. + +The grammar is: + +expression: expr? EOF +expr: and_expr ('or' and_expr)* +and_expr: not_expr ('and' not_expr)* +not_expr: 'not' not_expr | '(' expr ')' | ident +ident: (\w|:|\+|-|\.|\[|\])+ + +The semantics are: + +- Empty expression evaluates to False. +- ident evaluates to True of False according to a provided matcher function. +- or/and/not evaluate according to the usual boolean semantics. +""" +import enum +import re +from typing import Callable +from typing import Iterator +from typing import Optional +from typing import Sequence + +import attr + +from _pytest.compat import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import NoReturn + + +__all__ = [ + "evaluate", + "ParseError", +] + + +class TokenType(enum.Enum): + LPAREN = "left parenthesis" + RPAREN = "right parenthesis" + OR = "or" + AND = "and" + NOT = "not" + IDENT = "identifier" + EOF = "end of input" + + +@attr.s(frozen=True, slots=True) +class Token: + type = attr.ib(type=TokenType) + value = attr.ib(type=str) + pos = attr.ib(type=int) + + +class ParseError(Exception): + """The expression contains invalid syntax. + + :param column: The column in the line where the error occurred (1-based). + :param message: A description of the error. + """ + + def __init__(self, column: int, message: str) -> None: + self.column = column + self.message = message + + def __str__(self) -> str: + return "at column {}: {}".format(self.column, self.message) + + +class Scanner: + __slots__ = ("tokens", "current") + + def __init__(self, input: str) -> None: + self.tokens = self.lex(input) + self.current = next(self.tokens) + + def lex(self, input: str) -> Iterator[Token]: + pos = 0 + while pos < len(input): + if input[pos] in (" ", "\t"): + pos += 1 + elif input[pos] == "(": + yield Token(TokenType.LPAREN, "(", pos) + pos += 1 + elif input[pos] == ")": + yield Token(TokenType.RPAREN, ")", pos) + pos += 1 + else: + match = re.match(r"(:?\w|:|\+|-|\.|\[|\])+", input[pos:]) + if match: + value = match.group(0) + if value == "or": + yield Token(TokenType.OR, value, pos) + elif value == "and": + yield Token(TokenType.AND, value, pos) + elif value == "not": + yield Token(TokenType.NOT, value, pos) + else: + yield Token(TokenType.IDENT, value, pos) + pos += len(value) + else: + raise ParseError( + pos + 1, 'unexpected character "{}"'.format(input[pos]), + ) + yield Token(TokenType.EOF, "", pos) + + def accept(self, type: TokenType, *, reject: bool = False) -> Optional[Token]: + if self.current.type is type: + token = self.current + if token.type is not TokenType.EOF: + self.current = next(self.tokens) + return token + if reject: + self.reject((type,)) + return None + + def reject(self, expected: Sequence[TokenType]) -> "NoReturn": + raise ParseError( + self.current.pos + 1, + "expected {}; got {}".format( + " OR ".join(type.value for type in expected), self.current.type.value, + ), + ) + + +def expression(s: Scanner, matcher: Callable[[str], bool]) -> bool: + if s.accept(TokenType.EOF): + return False + ret = expr(s, matcher) + s.accept(TokenType.EOF, reject=True) + return ret + + +def expr(s: Scanner, matcher: Callable[[str], bool]) -> bool: + ret = and_expr(s, matcher) + while s.accept(TokenType.OR): + rhs = and_expr(s, matcher) + ret = ret or rhs + return ret + + +def and_expr(s: Scanner, matcher: Callable[[str], bool]) -> bool: + ret = not_expr(s, matcher) + while s.accept(TokenType.AND): + rhs = not_expr(s, matcher) + ret = ret and rhs + return ret + + +def not_expr(s: Scanner, matcher: Callable[[str], bool]) -> bool: + if s.accept(TokenType.NOT): + return not not_expr(s, matcher) + if s.accept(TokenType.LPAREN): + ret = expr(s, matcher) + s.accept(TokenType.RPAREN, reject=True) + return ret + ident = s.accept(TokenType.IDENT) + if ident: + return matcher(ident.value) + s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) + + +def evaluate(input: str, matcher: Callable[[str], bool]) -> bool: + """Evaluate a match expression as used by -k and -m. + + :param input: The input expression - one line. + :param matcher: Given an identifier, should return whether it matches or not. + Should be prepared to handle arbitrary strings as input. + + Returns whether the entire expression matches or not. + """ + return expression(Scanner(input), matcher) diff --git a/src/_pytest/mark/legacy.py b/src/_pytest/mark/legacy.py index eb50340f249..a9461d4cef2 100644 --- a/src/_pytest/mark/legacy.py +++ b/src/_pytest/mark/legacy.py @@ -2,44 +2,46 @@ this is a place where we put datastructures used by legacy apis we hope to remove """ -import keyword from typing import Set import attr from _pytest.compat import TYPE_CHECKING from _pytest.config import UsageError +from _pytest.mark.expression import evaluate +from _pytest.mark.expression import ParseError if TYPE_CHECKING: from _pytest.nodes import Item # noqa: F401 (used in type string) @attr.s -class MarkMapping: - """Provides a local mapping for markers where item access - resolves to True if the marker is present. """ +class MarkMatcher: + """A matcher for markers which are present.""" own_mark_names = attr.ib() @classmethod - def from_item(cls, item): + def from_item(cls, item) -> "MarkMatcher": mark_names = {mark.name for mark in item.iter_markers()} return cls(mark_names) - def __getitem__(self, name): + def __call__(self, name: str) -> bool: return name in self.own_mark_names @attr.s -class KeywordMapping: - """Provides a local mapping for keywords. - Given a list of names, map any substring of one of these names to True. +class KeywordMatcher: + """A matcher for keywords. + + Given a list of names, matches any substring of one of these names. The + string inclusion check is case-insensitive. """ _names = attr.ib(type=Set[str]) @classmethod - def from_item(cls, item: "Item") -> "KeywordMapping": + def from_item(cls, item: "Item") -> "KeywordMatcher": mapped_names = set() # Add the names of the current item and any parent items @@ -62,12 +64,7 @@ def from_item(cls, item: "Item") -> "KeywordMapping": return cls(mapped_names) - def __getitem__(self, subname: str) -> bool: - """Return whether subname is included within stored names. - - The string inclusion check is case-insensitive. - - """ + def __call__(self, subname: str) -> bool: subname = subname.lower() names = (name.lower() for name in self._names) @@ -77,18 +74,17 @@ def __getitem__(self, subname: str) -> bool: return False -python_keywords_allowed_list = ["or", "and", "not"] - - -def matchmark(colitem, markexpr): +def matchmark(colitem, markexpr: str) -> bool: """Tries to match on any marker names, attached to the given colitem.""" try: - return eval(markexpr, {}, MarkMapping.from_item(colitem)) - except Exception: - raise UsageError("Wrong expression passed to '-m': {}".format(markexpr)) + return evaluate(markexpr, MarkMatcher.from_item(colitem)) + except ParseError as e: + raise UsageError( + "Wrong expression passed to '-m': {}: {}".format(markexpr, e) + ) from None -def matchkeyword(colitem, keywordexpr): +def matchkeyword(colitem, keywordexpr: str) -> bool: """Tries to match given keyword expression to given collector item. Will match on the name of colitem, including the names of its parents. @@ -97,20 +93,9 @@ def matchkeyword(colitem, keywordexpr): Additionally, matches on names in the 'extra_keyword_matches' set of any item, as well as names directly assigned to test functions. """ - mapping = KeywordMapping.from_item(colitem) - if " " not in keywordexpr: - # special case to allow for simple "-k pass" and "-k 1.3" - return mapping[keywordexpr] - elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]: - return not mapping[keywordexpr[4:]] - for kwd in keywordexpr.split(): - if keyword.iskeyword(kwd) and kwd not in python_keywords_allowed_list: - raise UsageError( - "Python keyword '{}' not accepted in expressions passed to '-k'".format( - kwd - ) - ) try: - return eval(keywordexpr, {}, mapping) - except Exception: - raise UsageError("Wrong expression passed to '-k': {}".format(keywordexpr)) + return evaluate(keywordexpr, KeywordMatcher.from_item(colitem)) + except ParseError as e: + raise UsageError( + "Wrong expression passed to '-k': {}: {}".format(keywordexpr, e) + ) from None diff --git a/testing/test_mark.py b/testing/test_mark.py index 2aad2b1ba5a..30a18b38e7d 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -200,6 +200,8 @@ def test_hello(): "spec", [ ("xyz", ("test_one",)), + ("((( xyz)) )", ("test_one",)), + ("not not xyz", ("test_one",)), ("xyz and xyz2", ()), ("xyz2", ("test_two",)), ("xyz or xyz2", ("test_one", "test_two")), @@ -258,9 +260,11 @@ def test_nointer(): "spec", [ ("interface", ("test_interface",)), - ("not interface", ("test_nointer", "test_pass")), + ("not interface", ("test_nointer", "test_pass", "test_1", "test_2")), ("pass", ("test_pass",)), - ("not pass", ("test_interface", "test_nointer")), + ("not pass", ("test_interface", "test_nointer", "test_1", "test_2")), + ("not not not (pass)", ("test_interface", "test_nointer", "test_1", "test_2")), + ("1 or 2", ("test_1", "test_2")), ], ) def test_keyword_option_custom(spec, testdir): @@ -272,6 +276,10 @@ def test_nointer(): pass def test_pass(): pass + def test_1(): + pass + def test_2(): + pass """ ) opt, passed_result = spec @@ -293,7 +301,7 @@ def test_keyword_option_considers_mark(testdir): "spec", [ ("None", ("test_func[None]",)), - ("1.3", ("test_func[1.3]",)), + ("[1.3]", ("test_func[1.3]",)), ("2-3", ("test_func[2-3]",)), ], ) @@ -333,10 +341,23 @@ def test_func(arg): "spec", [ ( - "foo or import", - "ERROR: Python keyword 'import' not accepted in expressions passed to '-k'", + "foo or", + "at column 7: expected not OR left parenthesis OR identifier; got end of input", + ), + ( + "foo or or", + "at column 8: expected not OR left parenthesis OR identifier; got or", + ), + ("(foo", "at column 5: expected right parenthesis; got end of input",), + ("foo bar", "at column 5: expected end of input; got identifier",), + ( + "or or", + "at column 1: expected not OR left parenthesis OR identifier; got or", + ), + ( + "not or", + "at column 5: expected not OR left parenthesis OR identifier; got or", ), - ("foo or", "ERROR: Wrong expression passed to '-k': foo or"), ], ) def test_keyword_option_wrong_arguments(spec, testdir, capsys): @@ -798,10 +819,12 @@ def test_one(): passed, skipped, failed = reprec.countoutcomes() assert passed + skipped + failed == 0 - def test_no_magic_values(self, testdir): + @pytest.mark.parametrize( + "keyword", ["__", "+", ".."], + ) + def test_no_magic_values(self, testdir, keyword: str) -> None: """Make sure the tests do not match on magic values, - no double underscored values, like '__dict__', - and no instance values, like '()'. + no double underscored values, like '__dict__' and '+'. """ p = testdir.makepyfile( """ @@ -809,16 +832,12 @@ def test_one(): assert 1 """ ) - def assert_test_is_not_selected(keyword): - reprec = testdir.inline_run("-k", keyword, p) - passed, skipped, failed = reprec.countoutcomes() - dlist = reprec.getcalls("pytest_deselected") - assert passed + skipped + failed == 0 - deselected_tests = dlist[0].items - assert len(deselected_tests) == 1 - - assert_test_is_not_selected("__") - assert_test_is_not_selected("()") + reprec = testdir.inline_run("-k", keyword, p) + passed, skipped, failed = reprec.countoutcomes() + dlist = reprec.getcalls("pytest_deselected") + assert passed + skipped + failed == 0 + deselected_tests = dlist[0].items + assert len(deselected_tests) == 1 class TestMarkDecorator: @@ -1023,7 +1042,7 @@ def test_foo(): pass """ ) - expected = "ERROR: Wrong expression passed to '-m': {}".format(expr) + expected = "ERROR: Wrong expression passed to '-m': {}: *".format(expr) result = testdir.runpytest(foo, "-m", expr) result.stderr.fnmatch_lines([expected]) assert result.ret == ExitCode.USAGE_ERROR diff --git a/testing/test_mark_expression.py b/testing/test_mark_expression.py new file mode 100644 index 00000000000..74576786d13 --- /dev/null +++ b/testing/test_mark_expression.py @@ -0,0 +1,162 @@ +import pytest +from _pytest.mark.expression import evaluate +from _pytest.mark.expression import ParseError + + +def test_empty_is_false() -> None: + assert not evaluate("", lambda ident: False) + assert not evaluate("", lambda ident: True) + assert not evaluate(" ", lambda ident: False) + assert not evaluate("\t", lambda ident: False) + + +@pytest.mark.parametrize( + ("expr", "expected"), + ( + ("true", True), + ("true", True), + ("false", False), + ("not true", False), + ("not false", True), + ("not not true", True), + ("not not false", False), + ("true and true", True), + ("true and false", False), + ("false and true", False), + ("true and true and true", True), + ("true and true and false", False), + ("true and true and not true", False), + ("false or false", False), + ("false or true", True), + ("true or true", True), + ("true or true or false", True), + ("true and true or false", True), + ("not true or true", True), + ("(not true) or true", True), + ("not (true or true)", False), + ("true and true or false and false", True), + ("true and (true or false) and false", False), + ("true and (true or (not (not false))) and false", False), + ), +) +def test_basic(expr: str, expected: bool) -> None: + matcher = {"true": True, "false": False}.__getitem__ + assert evaluate(expr, matcher) is expected + + +@pytest.mark.parametrize( + ("expr", "expected"), + ( + (" true ", True), + (" ((((((true)))))) ", True), + (" ( ((\t (((true))))) \t \t)", True), + ("( true and (((false))))", False), + ("not not not not true", True), + ("not not not not not true", False), + ), +) +def test_syntax_oddeties(expr: str, expected: bool) -> None: + matcher = {"true": True, "false": False}.__getitem__ + assert evaluate(expr, matcher) is expected + + +@pytest.mark.parametrize( + ("expr", "column", "message"), + ( + ("(", 2, "expected not OR left parenthesis OR identifier; got end of input"), + (" (", 3, "expected not OR left parenthesis OR identifier; got end of input",), + ( + ")", + 1, + "expected not OR left parenthesis OR identifier; got right parenthesis", + ), + ( + ") ", + 1, + "expected not OR left parenthesis OR identifier; got right parenthesis", + ), + ("not", 4, "expected not OR left parenthesis OR identifier; got end of input",), + ( + "not not", + 8, + "expected not OR left parenthesis OR identifier; got end of input", + ), + ( + "(not)", + 5, + "expected not OR left parenthesis OR identifier; got right parenthesis", + ), + ("and", 1, "expected not OR left parenthesis OR identifier; got and"), + ( + "ident and", + 10, + "expected not OR left parenthesis OR identifier; got end of input", + ), + ("ident and or", 11, "expected not OR left parenthesis OR identifier; got or",), + ("ident ident", 7, "expected end of input; got identifier"), + ), +) +def test_syntax_errors(expr: str, column: int, message: str) -> None: + with pytest.raises(ParseError) as excinfo: + evaluate(expr, lambda ident: True) + assert excinfo.value.column == column + assert excinfo.value.message == message + + +@pytest.mark.parametrize( + "ident", + ( + ".", + "...", + ":::", + "a:::c", + "a+-b", + "אבגד", + "aaאבגדcc", + "a[bcd]", + "1234", + "1234abcd", + "1234and", + "notandor", + "not_and_or", + "not[and]or", + "1234+5678", + "123.232", + "True", + "False", + "if", + "else", + "while", + ), +) +def test_valid_idents(ident: str) -> None: + assert evaluate(ident, {ident: True}.__getitem__) + + +@pytest.mark.parametrize( + "ident", + ( + "/", + "\\", + "^", + "*", + "=", + "&", + "%", + "$", + "#", + "@", + "!", + "~", + "{", + "}", + '"', + "'", + "|", + ";", + "←", + ), +) +def test_invalid_idents(ident: str) -> None: + with pytest.raises(ParseError): + evaluate(ident, lambda ident: True) From 7f5978c34c3863fb5d7a7ea1fb914bbd056b7965 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 1 May 2020 10:58:47 -0300 Subject: [PATCH 147/823] Allow File.from_parent to forward custom parameters to the constructor --- changelog/7143.bugfix.rst | 1 + src/_pytest/nodes.py | 4 ++-- testing/test_collection.py | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 changelog/7143.bugfix.rst diff --git a/changelog/7143.bugfix.rst b/changelog/7143.bugfix.rst new file mode 100644 index 00000000000..abf47dd0c2d --- /dev/null +++ b/changelog/7143.bugfix.rst @@ -0,0 +1 @@ +Fix ``File.from_constructor`` so it forwards extra keyword arguments to the constructor. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 6761aa79c52..03a4b1af8a2 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -483,11 +483,11 @@ def __init__( self._norecursepatterns = self.config.getini("norecursedirs") @classmethod - def from_parent(cls, parent, *, fspath): + def from_parent(cls, parent, *, fspath, **kw): """ The public constructor """ - return super().from_parent(parent=parent, fspath=fspath) + return super().from_parent(parent=parent, fspath=fspath, **kw) def _gethookproxy(self, fspath: py.path.local): # check if we have the common case of running diff --git a/testing/test_collection.py b/testing/test_collection.py index 050b5459812..2fd832dc6b7 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1332,3 +1332,24 @@ def test_does_not_put_src_on_path(testdir): ) result = testdir.runpytest() assert result.ret == ExitCode.OK + + +def test_fscollector_from_parent(tmpdir, request): + """Ensure File.from_parent can forward custom arguments to the constructor. + + Context: https://github.com/pytest-dev/pytest-cpp/pull/47 + """ + + class MyCollector(pytest.File): + def __init__(self, fspath, parent, x): + super().__init__(fspath, parent) + self.x = x + + @classmethod + def from_parent(cls, parent, *, fspath, x): + return super().from_parent(parent=parent, fspath=fspath, x=x) + + collector = MyCollector.from_parent( + parent=request.session, fspath=tmpdir / "foo", x=10 + ) + assert collector.x == 10 From 5c378989497a4073ab2d4435e81beb1a6f8b5e46 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 1 May 2020 14:39:09 -0300 Subject: [PATCH 148/823] Fix some typos in the CHANGELOG --- doc/en/changelog.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index ac3b4ad85ef..00ed5497504 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -91,12 +91,12 @@ Deprecations - `#5975 `_: Deprecate using direct constructors for ``Nodes``. - Instead they are new constructed via ``Node.from_parent``. + Instead they are now constructed via ``Node.from_parent``. - This transitional mechanism enables us to detangle the very intensely - entangled ``Node`` relationships by enforcing more controlled creation/configruation patterns. + This transitional mechanism enables us to untangle the very intensely + entangled ``Node`` relationships by enforcing more controlled creation/configuration patterns. - As part of that session/config are already disallowed parameters and as we work on the details we might need disallow a few more as well. + As part of this change, session/config are already disallowed parameters and as we work on the details we might need disallow a few more as well. Subclasses are expected to use `super().from_parent` if they intend to expand the creation of `Nodes`. From fd2f172258d120912b6cb5fb2b9e7eab714b6098 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 1 May 2020 12:56:06 -0300 Subject: [PATCH 149/823] Let unittest frameworks deal with async functions Instead of trying to handle unittest-async functions in pytest_pyfunc_call, let the unittest framework handle them instead. This lets us remove the hack in pytest_pyfunc_call, with the upside that we should support any unittest-async based framework. Also included 'asynctest' as test dependency for py37-twisted, and renamed 'twisted' to 'unittestextras' to better reflect that we install 'twisted' and 'asynctest' now. This also fixes the problem of cleanUp functions not being properly called for async functions. Fix #7110 Fix #6924 --- .github/workflows/main.yml | 2 +- changelog/7110.bugfix.rst | 1 + src/_pytest/compat.py | 7 +++++ src/_pytest/python.py | 30 ++++--------------- src/_pytest/unittest.py | 19 +++++++----- .../unittest/test_unittest_asyncio.py | 9 ++++++ .../unittest/test_unittest_asynctest.py | 22 ++++++++++++++ testing/test_unittest.py | 11 ++++++- tox.ini | 5 ++-- 9 files changed, 70 insertions(+), 36 deletions(-) create mode 100644 changelog/7110.bugfix.rst create mode 100644 testing/example_scripts/unittest/test_unittest_asynctest.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 80317f1c15c..7228355831f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -70,7 +70,7 @@ jobs: - name: "windows-py38" python: "3.8" os: windows-latest - tox_env: "py38-twisted" + tox_env: "py38-unittestextras" use_coverage: true - name: "ubuntu-py35" diff --git a/changelog/7110.bugfix.rst b/changelog/7110.bugfix.rst new file mode 100644 index 00000000000..935f6ea3c85 --- /dev/null +++ b/changelog/7110.bugfix.rst @@ -0,0 +1 @@ +Fixed regression: ``asyncbase.TestCase`` tests are executed correctly again. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 8aff8d57da4..cf051182fd9 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -93,6 +93,13 @@ def syntax, and doesn't contain yield), or a function decorated with return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False) +def is_async_function(func: object) -> bool: + """Return True if the given function seems to be an async function or async generator""" + return iscoroutinefunction(func) or ( + sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func) + ) + + def getlocation(function, curdir=None) -> str: function = get_real_func(function) fn = py.path.local(inspect.getfile(function)) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2b9bf4f5bb5..e1bd62f0bb7 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -34,8 +34,8 @@ from _pytest.compat import get_real_func from _pytest.compat import getimfunc from _pytest.compat import getlocation +from _pytest.compat import is_async_function from _pytest.compat import is_generator -from _pytest.compat import iscoroutinefunction from _pytest.compat import NOTSET from _pytest.compat import REGEX_TYPE from _pytest.compat import safe_getattr @@ -159,7 +159,7 @@ def pytest_configure(config): ) -def async_warn(nodeid: str) -> None: +def async_warn_and_skip(nodeid: str) -> None: msg = "async def functions are not natively supported and have been skipped.\n" msg += ( "You need to install a suitable plugin for your async framework, for example:\n" @@ -175,33 +175,13 @@ def async_warn(nodeid: str) -> None: @hookimpl(trylast=True) def pytest_pyfunc_call(pyfuncitem: "Function"): testfunction = pyfuncitem.obj - - try: - # ignoring type as the import is invalid in py37 and mypy thinks its a error - from unittest import IsolatedAsyncioTestCase # type: ignore - except ImportError: - async_ok_in_stdlib = False - else: - async_ok_in_stdlib = isinstance( - getattr(testfunction, "__self__", None), IsolatedAsyncioTestCase - ) - - if ( - iscoroutinefunction(testfunction) - or (sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction)) - ) and not async_ok_in_stdlib: - async_warn(pyfuncitem.nodeid) + if is_async_function(testfunction): + async_warn_and_skip(pyfuncitem.nodeid) funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} result = testfunction(**testargs) if hasattr(result, "__await__") or hasattr(result, "__aiter__"): - if async_ok_in_stdlib: - # todo: investigate moving this to the unittest plugin - # by a test call result hook - testcase = testfunction.__self__ - testcase._callMaybeAsync(lambda: result) - else: - async_warn(pyfuncitem.nodeid) + async_warn_and_skip(pyfuncitem.nodeid) return True diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 2047876e5f1..e461248b73b 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -6,6 +6,7 @@ import _pytest._code import pytest from _pytest.compat import getimfunc +from _pytest.compat import is_async_function from _pytest.config import hookimpl from _pytest.outcomes import exit from _pytest.outcomes import fail @@ -227,13 +228,17 @@ def wrapped_testMethod(*args, **kwargs): self._needs_explicit_tearDown = True raise _GetOutOf_testPartExecutor(exc) - setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod) - try: - self._testcase(result=self) - except _GetOutOf_testPartExecutor as exc: - raise exc.args[0] from exc.args[0] - finally: - delattr(self._testcase, self._testcase._testMethodName) + # let the unittest framework handle async functions + if is_async_function(self.obj): + self._testcase(self) + else: + setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod) + try: + self._testcase(result=self) + except _GetOutOf_testPartExecutor as exc: + raise exc.args[0] from exc.args[0] + finally: + delattr(self._testcase, self._testcase._testMethodName) def _prunetraceback(self, excinfo): Function._prunetraceback(self, excinfo) diff --git a/testing/example_scripts/unittest/test_unittest_asyncio.py b/testing/example_scripts/unittest/test_unittest_asyncio.py index 16eec1026ff..76eebf74a5e 100644 --- a/testing/example_scripts/unittest/test_unittest_asyncio.py +++ b/testing/example_scripts/unittest/test_unittest_asyncio.py @@ -1,7 +1,13 @@ from unittest import IsolatedAsyncioTestCase # type: ignore +teardowns = [] + + class AsyncArguments(IsolatedAsyncioTestCase): + async def asyncTearDown(self): + teardowns.append(None) + async def test_something_async(self): async def addition(x, y): return x + y @@ -13,3 +19,6 @@ async def addition(x, y): return x + y self.assertEqual(await addition(2, 2), 3) + + def test_teardowns(self): + assert len(teardowns) == 2 diff --git a/testing/example_scripts/unittest/test_unittest_asynctest.py b/testing/example_scripts/unittest/test_unittest_asynctest.py new file mode 100644 index 00000000000..bddbe250a6b --- /dev/null +++ b/testing/example_scripts/unittest/test_unittest_asynctest.py @@ -0,0 +1,22 @@ +"""Issue #7110""" +import asyncio + +import asynctest + + +teardowns = [] + + +class Test(asynctest.TestCase): + async def tearDown(self): + teardowns.append(None) + + async def test_error(self): + await asyncio.sleep(0) + self.fail("failing on purpose") + + async def test_ok(self): + await asyncio.sleep(0) + + def test_teardowns(self): + assert len(teardowns) == 2 diff --git a/testing/test_unittest.py b/testing/test_unittest.py index de51f7bd104..a026dc3f6ef 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1136,4 +1136,13 @@ def test_async_support(testdir): testdir.copy_example("unittest/test_unittest_asyncio.py") reprec = testdir.inline_run() - reprec.assertoutcome(failed=1, passed=1) + reprec.assertoutcome(failed=1, passed=2) + + +def test_asynctest_support(testdir): + """Check asynctest support (#7110)""" + pytest.importorskip("asynctest") + + testdir.copy_example("unittest/test_unittest_asynctest.py") + reprec = testdir.inline_run() + reprec.assertoutcome(failed=1, passed=2) diff --git a/tox.ini b/tox.ini index 3a280abb782..8f23e3cf918 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ envlist = py37 py38 pypy3 - py37-{pexpect,xdist,twisted,numpy,pluggymaster} + py37-{pexpect,xdist,unittestextras,numpy,pluggymaster} doctesting py37-freeze docs @@ -49,7 +49,8 @@ deps = pexpect: pexpect pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master pygments - twisted: twisted + unittestextras: twisted + unittestextras: asynctest xdist: pytest-xdist>=1.13 {env:_PYTEST_TOX_EXTRA_DEP:} From 095a195d71d59f4f9a7332d2ee3fd77f395a7b92 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 2 May 2020 09:55:34 -0300 Subject: [PATCH 150/823] Improve docs about junit_family warning message From discussion in #6178 --- doc/en/deprecations.rst | 32 +++++++++++++++++++++++++------- src/_pytest/deprecated.py | 6 +++--- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 4d8177a543d..bd7a6ac99e6 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -80,22 +80,40 @@ Note that ``from_parent`` should only be called with keyword arguments for the p .. deprecated:: 5.2 -The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given -that this is the version supported by default in modern tools that manipulate this type of file. +The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, which +is an update of the old ``xunit1`` format and is supported by default in modern tools +that manipulate this type of file (for example, Jenkins, Azure Pipelines, etc.). -In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option -is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``:: +Users are recommended to try the new ``xunit2`` format and see if their tooling that consumes the JUnit +XML file supports it. - PytestDeprecationWarning: The 'junit_family' default value will change to 'xunit2' in pytest 6.0. - Add 'junit_family=legacy' to your pytest.ini file to silence this warning and make your suite compatible. +To use the new format, update your ``pytest.ini``: -In order to silence this warning, users just need to configure the ``junit_family`` option explicitly: +.. code-block:: ini + + [pytest] + junit_family=xunit2 + +If you discover that your tooling does not support the new format, and want to keep using the +legacy version, set the option to ``legacy`` instead: .. code-block:: ini [pytest] junit_family=legacy +By using ``legacy`` you will keep using the legacy/xunit1 format when upgrading to +pytest 6.0, where the default format will be ``xunit2``. + +In order to let users know about the transition, pytest will issue a warning in case +the ``--junitxml`` option is given in the command line but ``junit_family`` is not explicitly +configured in ``pytest.ini``. + +Services known to support the ``xunit2`` format: + +* `Jenkins `__ with the `JUnit `__ plugin. +* `Azure Pipelines `__. + ``funcargnames`` alias for ``fixturenames`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 1896a05405f..8c9bd9d5c42 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -49,9 +49,9 @@ ) JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning( - "The 'junit_family' default value will change to 'xunit2' in pytest 6.0.\n" - "Add 'junit_family=xunit1' to your pytest.ini file to keep the current format " - "in future versions of pytest and silence this warning." + "The 'junit_family' default value will change to 'xunit2' in pytest 6.0. See:\n" + " https://docs.pytest.org/en/latest/deprecations.html#junit-family-default-value-change-to-xunit2\n" + "for more information." ) NO_PRINT_LOGS = PytestDeprecationWarning( From 5c2e96c0e65bea2b733a5ccd94507d95e8cba12b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 1 May 2020 17:21:15 -0300 Subject: [PATCH 151/823] Fix cleanup functions not being invoked on test failures Also delay calling tearDown() when --pdb is given, so users still have access to the instance variables (which are usually cleaned up during tearDown()) when debugging. Fix #6947 --- changelog/6947.bugfix.rst | 1 + src/_pytest/debugging.py | 15 +++++++-- src/_pytest/unittest.py | 50 +++++++++++++---------------- testing/test_unittest.py | 66 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 31 deletions(-) create mode 100644 changelog/6947.bugfix.rst diff --git a/changelog/6947.bugfix.rst b/changelog/6947.bugfix.rst new file mode 100644 index 00000000000..3168df8434c --- /dev/null +++ b/changelog/6947.bugfix.rst @@ -0,0 +1 @@ +Fix regression where functions registered with ``TestCase.addCleanup`` were not being called on test failures. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 9155d7e98e3..17915db73fc 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -272,11 +272,15 @@ def pytest_internalerror(self, excrepr, excinfo): class PdbTrace: @hookimpl(hookwrapper=True) def pytest_pyfunc_call(self, pyfuncitem): - _test_pytest_function(pyfuncitem) + wrap_pytest_function_for_tracing(pyfuncitem) yield -def _test_pytest_function(pyfuncitem): +def wrap_pytest_function_for_tracing(pyfuncitem): + """Changes the python function object of the given Function item by a wrapper which actually + enters pdb before calling the python function itself, effectively leaving the user + in the pdb prompt in the first statement of the function. + """ _pdb = pytestPDB._init_pdb("runcall") testfunction = pyfuncitem.obj @@ -291,6 +295,13 @@ def wrapper(*args, **kwargs): pyfuncitem.obj = wrapper +def maybe_wrap_pytest_function_for_tracing(pyfuncitem): + """Wrap the given pytestfunct item for tracing support if --trace was given in + the command line""" + if pyfuncitem.config.getvalue("trace"): + wrap_pytest_function_for_tracing(pyfuncitem) + + def _enter_pdb(node, excinfo, rep): # XXX we re-use the TerminalReporter's terminalwriter # because this seems to avoid some encoding related troubles diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index e461248b73b..fc3d1a51533 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -1,5 +1,4 @@ """ discovery and running of std-library "unittest" style tests. """ -import functools import sys import traceback @@ -114,15 +113,17 @@ class TestCaseFunction(Function): _testcase = None def setup(self): - self._needs_explicit_tearDown = False + # a bound method to be called during teardown() if set (see 'runtest()') + self._explicit_tearDown = None self._testcase = self.parent.obj(self.name) self._obj = getattr(self._testcase, self.name) if hasattr(self, "_request"): self._request._fillfixtures() def teardown(self): - if self._needs_explicit_tearDown: - self._testcase.tearDown() + if self._explicit_tearDown is not None: + self._explicit_tearDown() + self._explicit_tearDown = None self._testcase = None self._obj = None @@ -205,40 +206,31 @@ def _expecting_failure(self, test_method) -> bool: return bool(expecting_failure_class or expecting_failure_method) def runtest(self): - # TODO: move testcase reporter into separate class, this shouldnt be on item - import unittest + from _pytest.debugging import maybe_wrap_pytest_function_for_tracing - testMethod = getattr(self._testcase, self._testcase._testMethodName) - - class _GetOutOf_testPartExecutor(KeyboardInterrupt): - """Helper exception to get out of unittests's testPartExecutor (see TestCase.run).""" - - @functools.wraps(testMethod) - def wrapped_testMethod(*args, **kwargs): - """Wrap the original method to call into pytest's machinery, so other pytest - features can have a chance to kick in (notably --pdb)""" - try: - self.ihook.pytest_pyfunc_call(pyfuncitem=self) - except unittest.SkipTest: - raise - except Exception as exc: - expecting_failure = self._expecting_failure(testMethod) - if expecting_failure: - raise - self._needs_explicit_tearDown = True - raise _GetOutOf_testPartExecutor(exc) + maybe_wrap_pytest_function_for_tracing(self) # let the unittest framework handle async functions if is_async_function(self.obj): self._testcase(self) else: - setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod) + # when --pdb is given, we want to postpone calling tearDown() otherwise + # when entering the pdb prompt, tearDown() would have probably cleaned up + # instance variables, which makes it difficult to debug + # arguably we could always postpone tearDown(), but this changes the moment where the + # TestCase instance interacts with the results object, so better to only do it + # when absolutely needed + if self.config.getoption("usepdb"): + self._explicit_tearDown = self._testcase.tearDown + setattr(self._testcase, "tearDown", lambda *args: None) + + # we need to update the actual bound method with self.obj, because + # wrap_pytest_function_for_tracing replaces self.obj by a wrapper + setattr(self._testcase, self.name, self.obj) try: self._testcase(result=self) - except _GetOutOf_testPartExecutor as exc: - raise exc.args[0] from exc.args[0] finally: - delattr(self._testcase, self._testcase._testMethodName) + delattr(self._testcase, self.name) def _prunetraceback(self, excinfo): Function._prunetraceback(self, excinfo) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index a026dc3f6ef..c72ed6f7dcc 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -876,6 +876,37 @@ def test_notTornDown(): reprec.assertoutcome(passed=1, failed=1) +def test_cleanup_functions(testdir): + """Ensure functions added with addCleanup are always called after each test ends (#6947)""" + testdir.makepyfile( + """ + import unittest + + cleanups = [] + + class Test(unittest.TestCase): + + def test_func_1(self): + self.addCleanup(cleanups.append, "test_func_1") + + def test_func_2(self): + self.addCleanup(cleanups.append, "test_func_2") + assert 0 + + def test_func_3_check_cleanups(self): + assert cleanups == ["test_func_1", "test_func_2"] + """ + ) + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "*::test_func_1 PASSED *", + "*::test_func_2 FAILED *", + "*::test_func_3_check_cleanups PASSED *", + ] + ) + + def test_issue333_result_clearing(testdir): testdir.makeconftest( """ @@ -1131,6 +1162,41 @@ def test(self): assert result.ret == 0 +def test_pdb_teardown_called(testdir, monkeypatch): + """Ensure tearDown() is always called when --pdb is given in the command-line. + + We delay the normal tearDown() calls when --pdb is given, so this ensures we are calling + tearDown() eventually to avoid memory leaks when using --pdb. + """ + teardowns = [] + monkeypatch.setattr( + pytest, "test_pdb_teardown_called_teardowns", teardowns, raising=False + ) + + testdir.makepyfile( + """ + import unittest + import pytest + + class MyTestCase(unittest.TestCase): + + def tearDown(self): + pytest.test_pdb_teardown_called_teardowns.append(self.id()) + + def test_1(self): + pass + def test_2(self): + pass + """ + ) + result = testdir.runpytest_inprocess("--pdb") + result.stdout.fnmatch_lines("* 2 passed in *") + assert teardowns == [ + "test_pdb_teardown_called.MyTestCase.test_1", + "test_pdb_teardown_called.MyTestCase.test_2", + ] + + def test_async_support(testdir): pytest.importorskip("unittest.async_case") From 82f584b5a9975ad758797203f52a38ba1fdecbad Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 1 May 2020 17:56:01 -0300 Subject: [PATCH 152/823] Fix test_trial_error in test_unittest This reverts the test to the state before 04f27d4, which introduced the breaking change about addCleanup not being called properly for failed tests. --- testing/test_unittest.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index c72ed6f7dcc..83f1b6b2a85 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -537,28 +537,24 @@ def f(_): ) result.stdout.fnmatch_lines( [ - "test_trial_error.py::TC::test_four SKIPPED", + "test_trial_error.py::TC::test_four FAILED", "test_trial_error.py::TC::test_four ERROR", "test_trial_error.py::TC::test_one FAILED", "test_trial_error.py::TC::test_three FAILED", - "test_trial_error.py::TC::test_two SKIPPED", - "test_trial_error.py::TC::test_two ERROR", + "test_trial_error.py::TC::test_two FAILED", "*ERRORS*", "*_ ERROR at teardown of TC.test_four _*", - "NOTE: Incompatible Exception Representation, displaying natively:", - "*DelayedCalls*", - "*_ ERROR at teardown of TC.test_two _*", - "NOTE: Incompatible Exception Representation, displaying natively:", "*DelayedCalls*", "*= FAILURES =*", - # "*_ TC.test_four _*", - # "*NameError*crash*", + "*_ TC.test_four _*", + "*NameError*crash*", "*_ TC.test_one _*", "*NameError*crash*", "*_ TC.test_three _*", - "NOTE: Incompatible Exception Representation, displaying natively:", "*DelayedCalls*", - "*= 2 failed, 2 skipped, 2 errors in *", + "*_ TC.test_two _*", + "*NameError*crash*", + "*= 4 failed, 1 error in *", ] ) From 799dab5284e4c7ba7c185fe2fa43a5a5f92a0517 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 2 May 2020 21:56:52 -0300 Subject: [PATCH 153/823] Remove blueyed from TIDELIFT blueyed is no longer a member of the pytest-dev organization. --- TIDELIFT.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/TIDELIFT.rst b/TIDELIFT.rst index e7a19a9ab3b..45247c29cf6 100644 --- a/TIDELIFT.rst +++ b/TIDELIFT.rst @@ -23,7 +23,7 @@ members of the `contributors team`_ interested in receiving funding. The current list of contributors receiving funding are: -* `@blueyed`_ +* Contributors interested in receiving a part of the funds just need to submit a PR adding their name to the list. Contributors that want to stop receiving the funds should also submit a PR @@ -53,5 +53,4 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the .. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members .. _`agreement`: https://tidelift.com/docs/lifting/agreement -.. _`@blueyed`: https://github.com/blueyed .. _`@nicoddemus`: https://github.com/nicoddemus From 678440e46d90815ef11584ab62e2fea130823397 Mon Sep 17 00:00:00 2001 From: Keri Volans Date: Sat, 2 May 2020 18:54:23 +0100 Subject: [PATCH 154/823] 7018: Use internal version of make_numbered_dir --- AUTHORS | 1 + src/_pytest/pytester.py | 5 ++--- testing/test_assertrewrite.py | 7 ++----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/AUTHORS b/AUTHORS index cfbcf432b6c..b59ebc2a27f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -150,6 +150,7 @@ Karl O. Pinc Katarzyna Jachim Katarzyna Król Katerina Koukiou +Keri Volans Kevin Cox Kevin J. Foley Kodi B. Arfer diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 9fcda8f0bed..051ea3fc7e5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -35,6 +35,7 @@ from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Collector from _pytest.nodes import Item +from _pytest.pathlib import make_numbered_dir from _pytest.pathlib import Path from _pytest.python import Module from _pytest.reports import TestReport @@ -1256,9 +1257,7 @@ def runpytest_subprocess(self, *args, timeout=None) -> RunResult: Returns a :py:class:`RunResult`. """ __tracebackhide__ = True - p = py.path.local.make_numbered_dir( - prefix="runpytest-", keep=None, rootdir=self.tmpdir - ) + p = make_numbered_dir(root=Path(self.tmpdir), prefix="runpytest-") args = ("--basetemp=%s" % p,) + args plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 8cf4929662e..487a7d13320 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -10,8 +10,6 @@ import zipfile from functools import partial -import py - import _pytest._code import pytest from _pytest.assertion import util @@ -22,6 +20,7 @@ from _pytest.assertion.rewrite import PYTEST_TAG from _pytest.assertion.rewrite import rewrite_asserts from _pytest.config import ExitCode +from _pytest.pathlib import make_numbered_dir from _pytest.pathlib import Path @@ -822,9 +821,7 @@ def test_optimized(): "hello" assert test_optimized.__doc__ is None""" ) - p = py.path.local.make_numbered_dir( - prefix="runpytest-", keep=None, rootdir=testdir.tmpdir - ) + p = make_numbered_dir(root=Path(testdir.tmpdir), prefix="runpytest-") tmp = "--basetemp=%s" % p monkeypatch.setenv("PYTHONOPTIMIZE", "2") monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) From 402ee6fb9da7eb5e5d702b7794fb0ff6573816fc Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Sun, 3 May 2020 22:56:38 +0200 Subject: [PATCH 155/823] Relative path to invocationdir instead rootdir. --- changelog/7076.trivial.rst | 1 + src/_pytest/skipping.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/7076.trivial.rst diff --git a/changelog/7076.trivial.rst b/changelog/7076.trivial.rst new file mode 100644 index 00000000000..5d9749c69c4 --- /dev/null +++ b/changelog/7076.trivial.rst @@ -0,0 +1 @@ +The path of file skipped by ``@pytest.mark.skip`` in the SKIPPED report is now relative to invocation directory. Previously it was relative to root directory. diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index fe8742c6675..22e51bc4b7e 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -169,6 +169,7 @@ def pytest_runtest_makereport(item, call): # the location of where the skip exception was raised within pytest _, _, reason = rep.longrepr filename, line = item.location[:2] + filename = item.config.rootdir.join(filename) rep.longrepr = filename, line + 1, reason From a5bcd0655f7baa00f82d9a74bc2e62dbf63e8b4a Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Mon, 4 May 2020 00:04:38 +0200 Subject: [PATCH 156/823] Test relapth when rootdir != invocationdir. --- testing/test_skipping.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 8b1cdd527ad..e7edf139d22 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1176,3 +1176,23 @@ def test_importorskip(): match="^could not import 'doesnotexist': No module named .*", ): pytest.importorskip("doesnotexist") + + +def test_relpath_rootdir(testdir): + testdir.makepyfile( + **{ + "tests/test_1.py": """ + import pytest + @pytest.mark.skip() + def test_pass(): + pass + """, + "tests/sub_tests/test_empty.py": """ + pass + """, + } + ) + result = testdir.runpytest("-rs", "tests/test_1.py", "--rootdir=tests/sub_tests") + result.stdout.fnmatch_lines( + ["SKIPPED [[]1[]] tests/test_1.py:2: unconditional skip"] + ) From 9b423710aa79e5c388e7ccfd9f67946cf317307f Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Mon, 4 May 2020 00:34:19 +0200 Subject: [PATCH 157/823] Remove unnecessary file in test. --- testing/test_skipping.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index e7edf139d22..32634d78459 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1187,12 +1187,9 @@ def test_relpath_rootdir(testdir): def test_pass(): pass """, - "tests/sub_tests/test_empty.py": """ - pass - """, } ) - result = testdir.runpytest("-rs", "tests/test_1.py", "--rootdir=tests/sub_tests") + result = testdir.runpytest("-rs", "tests/test_1.py", "--rootdir=tests") result.stdout.fnmatch_lines( ["SKIPPED [[]1[]] tests/test_1.py:2: unconditional skip"] ) From 16a44823eb053ded01136c7110760c8639c7343b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 4 May 2020 19:09:29 -0300 Subject: [PATCH 158/823] Use reportinfo() instead of location in skipping message --- changelog/{7076.trivial.rst => 7076.bugfix.rst} | 0 src/_pytest/skipping.py | 5 ++--- 2 files changed, 2 insertions(+), 3 deletions(-) rename changelog/{7076.trivial.rst => 7076.bugfix.rst} (100%) diff --git a/changelog/7076.trivial.rst b/changelog/7076.bugfix.rst similarity index 100% rename from changelog/7076.trivial.rst rename to changelog/7076.bugfix.rst diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 22e51bc4b7e..62a9ca491e7 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -168,9 +168,8 @@ def pytest_runtest_makereport(item, call): # to point to the item definition, otherwise it will display # the location of where the skip exception was raised within pytest _, _, reason = rep.longrepr - filename, line = item.location[:2] - filename = item.config.rootdir.join(filename) - rep.longrepr = filename, line + 1, reason + filename, line = item.reportinfo()[:2] + rep.longrepr = str(filename), line + 1, reason # called by terminalreporter progress reporting From 7647d1c836274497d60c2830cc9a3f0698af52a2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 4 Feb 2020 09:49:11 +0100 Subject: [PATCH 159/823] Move Capture classes from compat to capture and improve naming Move {Passthrough,CaptureIO} to capture module, and rename Passthrough -> Tee to match the existing terminology. Co-authored-by: Ran Benita --- src/_pytest/capture.py | 24 +++++++++++++++++++++--- src/_pytest/compat.py | 21 --------------------- testing/test_capture.py | 6 +++--- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 2798a24b4c4..d34bf23f879 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -11,10 +11,9 @@ from tempfile import TemporaryFile from typing import Generator from typing import Optional +from typing import TextIO import pytest -from _pytest.compat import CaptureAndPassthroughIO -from _pytest.compat import CaptureIO from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.fixtures import FixtureRequest @@ -320,6 +319,25 @@ def capfdbinary(request): yield fixture +class CaptureIO(io.TextIOWrapper): + def __init__(self) -> None: + super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) + + def getvalue(self) -> str: + assert isinstance(self.buffer, io.BytesIO) + return self.buffer.getvalue().decode("UTF-8") + + +class TeeCaptureIO(CaptureIO): + def __init__(self, other: TextIO) -> None: + self._other = other + super().__init__() + + def write(self, s: str) -> int: + super().write(s) + return self._other.write(s) + + class CaptureFixture: """ Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary` @@ -673,7 +691,7 @@ def __init__(self, fd, tmpfile=None): if name == "stdin": tmpfile = DontReadFromInput() else: - tmpfile = CaptureAndPassthroughIO(self._old) + tmpfile = TeeCaptureIO(self._old) self.tmpfile = tmpfile diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index cf051182fd9..ba225eb8ff8 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -3,7 +3,6 @@ """ import functools import inspect -import io import os import re import sys @@ -13,7 +12,6 @@ from typing import Any from typing import Callable from typing import Generic -from typing import IO from typing import Optional from typing import overload from typing import Tuple @@ -343,25 +341,6 @@ def safe_isclass(obj: object) -> bool: return False -class CaptureIO(io.TextIOWrapper): - def __init__(self) -> None: - super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) - - def getvalue(self) -> str: - assert isinstance(self.buffer, io.BytesIO) - return self.buffer.getvalue().decode("UTF-8") - - -class CaptureAndPassthroughIO(CaptureIO): - def __init__(self, other: IO) -> None: - self._other = other - super().__init__() - - def write(self, s) -> int: - super().write(s) - return self._other.write(s) - - if sys.version_info < (3, 5, 2): def overload(f): # noqa: F811 diff --git a/testing/test_capture.py b/testing/test_capture.py index b059073b5c6..23314319351 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -804,10 +804,10 @@ def test_write_bytes_to_buffer(self): assert f.getvalue() == "foo\r\n" -class TestCaptureAndPassthroughIO(TestCaptureIO): +class TestTeeCaptureIO(TestCaptureIO): def test_text(self): sio = io.StringIO() - f = capture.CaptureAndPassthroughIO(sio) + f = capture.TeeCaptureIO(sio) f.write("hello") s1 = f.getvalue() assert s1 == "hello" @@ -818,7 +818,7 @@ def test_text(self): def test_unicode_and_str_mixture(self): sio = io.StringIO() - f = capture.CaptureAndPassthroughIO(sio) + f = capture.TeeCaptureIO(sio) f.write("\u00f6") pytest.raises(TypeError, f.write, b"hello") From 8cae78a18b53adb14e06d143eca8e97b961002e6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 5 Apr 2020 16:10:05 +0200 Subject: [PATCH 160/823] Fix warnings summary - replace "tests with warnings" with just warnings: they a) might not come from a test in the first place, and b) a single test might have multiple warnings. - fix usage of (unused) argument to `collapsed_location_report` Co-authored-by: Ran Benita --- src/_pytest/terminal.py | 10 ++-- .../test_group_warnings_by_message.py | 15 ++++-- .../test_group_warnings_by_message_summary.py | 21 -------- .../test_1.py | 21 ++++++++ .../test_2.py | 5 ++ testing/test_warnings.py | 51 +++++++++++++------ 6 files changed, 75 insertions(+), 48 deletions(-) delete mode 100644 testing/example_scripts/warnings/test_group_warnings_by_message_summary.py create mode 100644 testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py create mode 100644 testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 52c04a49c3b..27467b6c735 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -840,7 +840,7 @@ def summary_warnings(self): def collapsed_location_report(reports: List[WarningReport]): locations = [] - for w in warning_reports: + for w in reports: location = w.get_location(self.config) if location: locations.append(location) @@ -852,16 +852,14 @@ def collapsed_location_report(reports: List[WarningReport]): str(loc).split("::", 1)[0] for loc in locations ) return "\n".join( - "{0}: {1} test{2} with warning{2}".format( - k, v, "s" if v > 1 else "" - ) + "{}: {} warning{}".format(k, v, "s" if v > 1 else "") for k, v in counts_by_filename.items() ) title = "warnings summary (final)" if final else "warnings summary" self.write_sep("=", title, yellow=True, bold=False) - for message, warning_reports in reports_grouped_by_message.items(): - maybe_location = collapsed_location_report(warning_reports) + for message, message_reports in reports_grouped_by_message.items(): + maybe_location = collapsed_location_report(message_reports) if maybe_location: self._tw.line(maybe_location) lines = message.splitlines() diff --git a/testing/example_scripts/warnings/test_group_warnings_by_message.py b/testing/example_scripts/warnings/test_group_warnings_by_message.py index c736135b7b9..6985caa4407 100644 --- a/testing/example_scripts/warnings/test_group_warnings_by_message.py +++ b/testing/example_scripts/warnings/test_group_warnings_by_message.py @@ -3,14 +3,19 @@ import pytest -def func(): - warnings.warn(UserWarning("foo")) +def func(msg): + warnings.warn(UserWarning(msg)) @pytest.mark.parametrize("i", range(5)) def test_foo(i): - func() + func("foo") -def test_bar(): - func() +def test_foo_1(): + func("foo") + + +@pytest.mark.parametrize("i", range(5)) +def test_bar(i): + func("bar") diff --git a/testing/example_scripts/warnings/test_group_warnings_by_message_summary.py b/testing/example_scripts/warnings/test_group_warnings_by_message_summary.py deleted file mode 100644 index 4f7df3d6d33..00000000000 --- a/testing/example_scripts/warnings/test_group_warnings_by_message_summary.py +++ /dev/null @@ -1,21 +0,0 @@ -import warnings - -import pytest - - -def func(): - warnings.warn(UserWarning("foo")) - - -@pytest.fixture(params=range(20), autouse=True) -def repeat_hack(request): - return request.param - - -@pytest.mark.parametrize("i", range(5)) -def test_foo(i): - func() - - -def test_bar(): - func() diff --git a/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py b/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py new file mode 100644 index 00000000000..b8c11cb71c9 --- /dev/null +++ b/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py @@ -0,0 +1,21 @@ +import warnings + +import pytest + + +def func(msg): + warnings.warn(UserWarning(msg)) + + +@pytest.mark.parametrize("i", range(20)) +def test_foo(i): + func("foo") + + +def test_foo_1(): + func("foo") + + +@pytest.mark.parametrize("i", range(20)) +def test_bar(i): + func("bar") diff --git a/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py b/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py new file mode 100644 index 00000000000..636d04a5505 --- /dev/null +++ b/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py @@ -0,0 +1,5 @@ +from test_1 import func + + +def test_2(): + func("foo") diff --git a/testing/test_warnings.py b/testing/test_warnings.py index bf7fe51c6f6..51d1286b465 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -574,35 +574,54 @@ def test_group_warnings_by_message(testdir): result = testdir.runpytest() result.stdout.fnmatch_lines( [ - "test_group_warnings_by_message.py::test_foo[0]", - "test_group_warnings_by_message.py::test_foo[1]", - "test_group_warnings_by_message.py::test_foo[2]", - "test_group_warnings_by_message.py::test_foo[3]", - "test_group_warnings_by_message.py::test_foo[4]", - "test_group_warnings_by_message.py::test_bar", - ] + "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + "test_group_warnings_by_message.py::test_foo[[]0[]]", + "test_group_warnings_by_message.py::test_foo[[]1[]]", + "test_group_warnings_by_message.py::test_foo[[]2[]]", + "test_group_warnings_by_message.py::test_foo[[]3[]]", + "test_group_warnings_by_message.py::test_foo[[]4[]]", + "test_group_warnings_by_message.py::test_foo_1", + " */test_group_warnings_by_message.py:*: UserWarning: foo", + " warnings.warn(UserWarning(msg))", + "", + "test_group_warnings_by_message.py::test_bar[[]0[]]", + "test_group_warnings_by_message.py::test_bar[[]1[]]", + "test_group_warnings_by_message.py::test_bar[[]2[]]", + "test_group_warnings_by_message.py::test_bar[[]3[]]", + "test_group_warnings_by_message.py::test_bar[[]4[]]", + " */test_group_warnings_by_message.py:*: UserWarning: bar", + " warnings.warn(UserWarning(msg))", + "", + "-- Docs: *", + "*= 11 passed, 11 warnings *", + ], + consecutive=True, ) - warning_code = 'warnings.warn(UserWarning("foo"))' - assert warning_code in result.stdout.str() - assert result.stdout.str().count(warning_code) == 1 @pytest.mark.filterwarnings("ignore::pytest.PytestExperimentalApiWarning") @pytest.mark.filterwarnings("always") def test_group_warnings_by_message_summary(testdir): - testdir.copy_example("warnings/test_group_warnings_by_message_summary.py") + testdir.copy_example("warnings/test_group_warnings_by_message_summary") + testdir.syspathinsert() result = testdir.runpytest() result.stdout.fnmatch_lines( [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, - "test_group_warnings_by_message_summary.py: 120 tests with warnings", - "*test_group_warnings_by_message_summary.py:7: UserWarning: foo", + "test_1.py: 21 warnings", + "test_2.py: 1 warning", + " */test_1.py:7: UserWarning: foo", + " warnings.warn(UserWarning(msg))", + "", + "test_1.py: 20 warnings", + " */test_1.py:7: UserWarning: bar", + " warnings.warn(UserWarning(msg))", + "", + "-- Docs: *", + "*= 42 passed, 42 warnings *", ], consecutive=True, ) - warning_code = 'warnings.warn(UserWarning("foo"))' - assert warning_code in result.stdout.str() - assert result.stdout.str().count(warning_code) == 1 def test_pytest_configure_warning(testdir, recwarn): From abf785666a72f3e3ed0aefce69497e9a0cde12be Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 5 May 2020 22:00:55 +0300 Subject: [PATCH 161/823] testing: fix lint after merge of old branch --- testing/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_config.py b/testing/test_config.py index bf388c729da..a01568608fe 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1465,7 +1465,7 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives warnings.filterwarnings('always', category=DeprecationWarning) pytest_plugins=['capture'] """, - } + }, ) res = testdir.runpytest_subprocess() assert res.ret == 0 From 94400a68b4a7ddc31b90092297d288c02ae2b33c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 5 May 2020 23:08:44 +0300 Subject: [PATCH 162/823] terminal: fix non-deterministic warning summary order in Python 3.5 In Python 3.5, collections.Counter() does not preserve insertion order. --- src/_pytest/terminal.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index bf1b65ed9e0..9c88ca0ca06 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -854,9 +854,12 @@ def collapsed_location_report(reports: List[WarningReport]): if len(locations) < 10: return "\n".join(map(str, locations)) - counts_by_filename = collections.Counter( - str(loc).split("::", 1)[0] for loc in locations - ) + counts_by_filename = ( + collections.OrderedDict() + ) # type: collections.OrderedDict[str, int] + for loc in locations: + key = str(loc).split("::", 1)[0] + counts_by_filename[key] = counts_by_filename.get(key, 0) + 1 return "\n".join( "{}: {} warning{}".format(k, v, "s" if v > 1 else "") for k, v in counts_by_filename.items() From d0022b5a13276f1e2fbcfc51a615b41147bf81c2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 5 May 2020 19:20:34 -0300 Subject: [PATCH 163/823] 'saferepr' handles classes with broken __getattribute__ Fix #7145 --- changelog/7145.bugfix.rst | 1 + src/_pytest/_io/saferepr.py | 2 +- testing/io/test_saferepr.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 changelog/7145.bugfix.rst diff --git a/changelog/7145.bugfix.rst b/changelog/7145.bugfix.rst new file mode 100644 index 00000000000..def237dc0c9 --- /dev/null +++ b/changelog/7145.bugfix.rst @@ -0,0 +1 @@ +Classes with broken ``__getattribute__`` methods are displayed correctly during failures. diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 23af4d0bb70..47a00de6063 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -20,7 +20,7 @@ def _format_repr_exception(exc: BaseException, obj: Any) -> str: except BaseException as exc: exc_info = "unpresentable exception ({})".format(_try_repr_or_str(exc)) return "<[{} raised in repr()] {} object at 0x{:x}>".format( - exc_info, obj.__class__.__name__, id(obj) + exc_info, type(obj).__name__, id(obj) ) diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 06084202eb7..f4ced8facdf 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -154,3 +154,20 @@ def test_pformat_dispatch(): assert _pformat_dispatch("a") == "'a'" assert _pformat_dispatch("a" * 10, width=5) == "'aaaaaaaaaa'" assert _pformat_dispatch("foo bar", width=5) == "('foo '\n 'bar')" + + +def test_broken_getattribute(): + """saferepr() can create proper representations of classes with + broken __getattribute__ (#7145) + """ + + class SomeClass: + def __getattribute__(self, attr): + raise RuntimeError + + def __repr__(self): + raise RuntimeError + + assert saferepr(SomeClass()).startswith( + "<[RuntimeError() raised in repr()] SomeClass object at 0x" + ) From 69143fe5b0a1037198b6ff1b151dc571e3800cec Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 20:54:46 +0300 Subject: [PATCH 164/823] code: fix import cycles between code.py and source.py These two files were really intertwined. Make it so code.py depends on source.py without a reverse dependency. No functional changes. --- src/_pytest/_code/__init__.py | 32 ++++++++++----- src/_pytest/_code/code.py | 71 +++++++++++++++++++--------------- src/_pytest/_code/source.py | 56 +++++++-------------------- src/_pytest/fixtures.py | 2 +- src/_pytest/mark/structures.py | 2 +- src/_pytest/nodes.py | 2 +- src/_pytest/python.py | 2 +- 7 files changed, 81 insertions(+), 86 deletions(-) diff --git a/src/_pytest/_code/__init__.py b/src/_pytest/_code/__init__.py index 370e41dc9f3..38019298c3c 100644 --- a/src/_pytest/_code/__init__.py +++ b/src/_pytest/_code/__init__.py @@ -1,10 +1,22 @@ -""" python inspection/code generation API """ -from .code import Code # noqa -from .code import ExceptionInfo # noqa -from .code import filter_traceback # noqa -from .code import Frame # noqa -from .code import getrawcode # noqa -from .code import Traceback # noqa -from .source import compile_ as compile # noqa -from .source import getfslineno # noqa -from .source import Source # noqa +"""Python inspection/code generation API.""" +from .code import Code +from .code import ExceptionInfo +from .code import filter_traceback +from .code import Frame +from .code import getfslineno +from .code import getrawcode +from .code import Traceback +from .source import compile_ as compile +from .source import Source + +__all__ = [ + "Code", + "ExceptionInfo", + "filter_traceback", + "Frame", + "getfslineno", + "getrawcode", + "Traceback", + "compile", + "Source", +] diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 02efc71722b..6102084f0b8 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -29,10 +29,15 @@ import py import _pytest +from _pytest._code.source import findsource +from _pytest._code.source import getrawcode +from _pytest._code.source import getstatementrange_ast +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 ATTRS_EQ_FIELD +from _pytest.compat import get_real_func from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING @@ -41,8 +46,6 @@ from typing_extensions import Literal from weakref import ReferenceType # noqa: F401 - from _pytest._code import Source - _TracebackStyle = Literal["long", "short", "line", "no", "native"] @@ -90,18 +93,14 @@ def path(self) -> Union[py.path.local, str]: def fullsource(self) -> Optional["Source"]: """ return a _pytest._code.Source object for the full source file of the code """ - from _pytest._code import source - - full, _ = source.findsource(self.raw) + full, _ = findsource(self.raw) return full def source(self) -> "Source": """ return a _pytest._code.Source object for the code object's source only """ # return source only for that part of code - import _pytest._code - - return _pytest._code.Source(self.raw) + return Source(self.raw) def getargs(self, var: bool = False) -> Tuple[str, ...]: """ return a tuple with the argument names for the code object @@ -132,10 +131,8 @@ def __init__(self, frame: FrameType) -> None: @property def statement(self) -> "Source": """ statement this frame is at """ - import _pytest._code - if self.code.fullsource is None: - return _pytest._code.Source("") + return Source("") return self.code.fullsource.getstatement(self.lineno) def eval(self, code, **vars): @@ -231,8 +228,6 @@ def getsource(self, astcache=None) -> Optional["Source"]: """ return failing source code. """ # we use the passed in astcache to not reparse asttrees # within exception info printing - from _pytest._code.source import getstatementrange_ast - source = self.frame.code.fullsource if source is None: return None @@ -703,11 +698,9 @@ def get_source( short: bool = False, ) -> List[str]: """ return formatted and marked up source lines. """ - import _pytest._code - lines = [] if source is None or line_index >= len(source.lines): - source = _pytest._code.Source("???") + source = Source("???") line_index = 0 if line_index < 0: line_index += len(source) @@ -769,11 +762,9 @@ def repr_locals(self, locals: Dict[str, object]) -> Optional["ReprLocals"]: def repr_traceback_entry( self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None ) -> "ReprEntry": - import _pytest._code - source = self._getentrysource(entry) if source is None: - source = _pytest._code.Source("???") + source = Source("???") line_index = 0 else: line_index = entry.lineno - entry.getfirstlinesource() @@ -1150,19 +1141,37 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line("") -def getrawcode(obj, trycall: bool = True): - """ return code object for given function. """ +def getfslineno(obj: Any) -> Tuple[Union[str, py.path.local], int]: + """ Return source location (path, lineno) for the given object. + If the source cannot be determined return ("", -1). + + The line number is 0-based. + """ + # xxx let decorators etc specify a sane ordering + # NOTE: this used to be done in _pytest.compat.getfslineno, initially added + # in 6ec13a2b9. It ("place_as") appears to be something very custom. + obj = get_real_func(obj) + if hasattr(obj, "place_as"): + obj = obj.place_as + try: - return obj.__code__ - except AttributeError: - obj = getattr(obj, "f_code", obj) - obj = getattr(obj, "__code__", obj) - if trycall and not hasattr(obj, "co_firstlineno"): - if hasattr(obj, "__call__") and not inspect.isclass(obj): - x = getrawcode(obj.__call__, trycall=False) - if hasattr(x, "co_firstlineno"): - return x - return obj + code = Code(obj) + except TypeError: + try: + fn = inspect.getsourcefile(obj) or inspect.getfile(obj) + except TypeError: + return "", -1 + + fspath = fn and py.path.local(fn) or "" + lineno = -1 + if fspath: + try: + _, lineno = findsource(obj) + except OSError: + pass + return fspath, lineno + else: + return code.path, code.firstlineno # relative paths that we use to filter traceback entries from appearing to the user; diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 2e44b69d249..3f732792fcf 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -8,7 +8,6 @@ from bisect import bisect_right from types import CodeType from types import FrameType -from typing import Any from typing import Iterator from typing import List from typing import Optional @@ -18,7 +17,6 @@ import py -from _pytest.compat import get_real_func from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING @@ -279,41 +277,6 @@ def compile_( # noqa: F811 return s.compile(filename, mode, flags, _genframe=_genframe) -def getfslineno(obj: Any) -> Tuple[Union[str, py.path.local], int]: - """ Return source location (path, lineno) for the given object. - If the source cannot be determined return ("", -1). - - The line number is 0-based. - """ - from .code import Code - - # xxx let decorators etc specify a sane ordering - # NOTE: this used to be done in _pytest.compat.getfslineno, initially added - # in 6ec13a2b9. It ("place_as") appears to be something very custom. - obj = get_real_func(obj) - if hasattr(obj, "place_as"): - obj = obj.place_as - - try: - code = Code(obj) - except TypeError: - try: - fn = inspect.getsourcefile(obj) or inspect.getfile(obj) - except TypeError: - return "", -1 - - fspath = fn and py.path.local(fn) or "" - lineno = -1 - if fspath: - try: - _, lineno = findsource(obj) - except OSError: - pass - return fspath, lineno - else: - return code.path, code.firstlineno - - # # helper functions # @@ -329,9 +292,22 @@ def findsource(obj) -> Tuple[Optional[Source], int]: return source, lineno -def getsource(obj, **kwargs) -> Source: - from .code import getrawcode +def getrawcode(obj, trycall: bool = True): + """ return code object for given function. """ + try: + return obj.__code__ + except AttributeError: + obj = getattr(obj, "f_code", obj) + obj = getattr(obj, "__code__", obj) + if trycall and not hasattr(obj, "co_firstlineno"): + if hasattr(obj, "__call__") and not inspect.isclass(obj): + x = getrawcode(obj.__call__, trycall=False) + if hasattr(x, "co_firstlineno"): + return x + return obj + +def getsource(obj, **kwargs) -> Source: obj = getrawcode(obj) try: strsrc = inspect.getsource(obj) @@ -346,8 +322,6 @@ def deindent(lines: Sequence[str]) -> List[str]: def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]: - import ast - # flatten all statements and except handlers into one lineno-list # AST's line numbers start indexing at 1 values = [] # type: List[int] diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f673885c731..fef82adac53 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -13,9 +13,9 @@ import py import _pytest +from _pytest._code import getfslineno from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr -from _pytest._code.source import getfslineno from _pytest._io import TerminalWriter from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index bcbfbd72ece..a34a0c28d05 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -14,7 +14,7 @@ import attr -from .._code.source import getfslineno +from .._code import getfslineno from ..compat import ascii_escaped from ..compat import NOTSET from _pytest.outcomes import fail diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 03a4b1af8a2..ad8f77ae844 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -12,10 +12,10 @@ import py import _pytest._code +from _pytest._code import getfslineno from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprExceptionInfo -from _pytest._code.source import getfslineno from _pytest.compat import cached_property from _pytest.compat import TYPE_CHECKING from _pytest.config import Config diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e1bd62f0bb7..9f4af9c625e 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -25,8 +25,8 @@ from _pytest import fixtures from _pytest import nodes from _pytest._code import filter_traceback +from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo -from _pytest._code.source import getfslineno from _pytest._io import TerminalWriter from _pytest._io.saferepr import saferepr from _pytest.compat import ascii_escaped From fcc473ab1c3891c410cc3ea362619a56731787a8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 15:49:20 +0300 Subject: [PATCH 165/823] Use dict instead of OrderedDict on Python 3.7 OrderedDict is quite a bit heavier than just a dict. --- src/_pytest/cacheprovider.py | 6 +++--- src/_pytest/compat.py | 12 ++++++++++++ src/_pytest/fixtures.py | 14 ++++++++------ src/_pytest/terminal.py | 10 ++++------ 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 1333522623e..a48bd9d6fbf 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -6,7 +6,6 @@ """ import json import os -from collections import OrderedDict from typing import Dict from typing import Generator from typing import List @@ -23,6 +22,7 @@ from .reports import CollectReport from _pytest import nodes from _pytest._io import TerminalWriter +from _pytest.compat import order_preserving_dict from _pytest.config import Config from _pytest.main import Session from _pytest.python import Module @@ -338,8 +338,8 @@ def pytest_collection_modifyitems( self, session: Session, config: Config, items: List[nodes.Item] ) -> None: if self.active: - new_items = OrderedDict() # type: OrderedDict[str, nodes.Item] - other_items = OrderedDict() # type: OrderedDict[str, nodes.Item] + new_items = order_preserving_dict() # type: Dict[str, nodes.Item] + other_items = order_preserving_dict() # type: Dict[str, nodes.Item] for item in items: if item.nodeid not in self.cached_nodeids: new_items[item.nodeid] = item diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index ba225eb8ff8..6a0614f0129 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -381,3 +381,15 @@ def __get__(self, instance, owner=None): # noqa: F811 return self value = instance.__dict__[self.func.__name__] = self.func(instance) return value + + +# Sometimes an algorithm needs a dict which yields items in the order in which +# they were inserted when iterated. Since Python 3.7, `dict` preserves +# insertion order. Since `dict` is faster and uses less memory than +# `OrderedDict`, prefer to use it if possible. +if sys.version_info >= (3, 7): + order_preserving_dict = dict +else: + from collections import OrderedDict + + order_preserving_dict = OrderedDict diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f673885c731..b7124fa80cc 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -4,7 +4,6 @@ import warnings from collections import defaultdict from collections import deque -from collections import OrderedDict from typing import Dict from typing import List from typing import Tuple @@ -26,6 +25,7 @@ from _pytest.compat import getlocation from _pytest.compat import is_generator from _pytest.compat import NOTSET +from _pytest.compat import order_preserving_dict from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING from _pytest.deprecated import FILLFUNCARGS @@ -220,12 +220,14 @@ def reorder_items(items): argkeys_cache[scopenum] = d = {} items_by_argkey[scopenum] = item_d = defaultdict(deque) for item in items: - keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum)) + keys = order_preserving_dict.fromkeys( + get_parametrized_fixture_keys(item, scopenum) + ) if keys: d[item] = keys for key in keys: item_d[key].append(item) - items = OrderedDict.fromkeys(items) + items = order_preserving_dict.fromkeys(items) return list(reorder_items_atscope(items, argkeys_cache, items_by_argkey, 0)) @@ -240,17 +242,17 @@ def reorder_items_atscope(items, argkeys_cache, items_by_argkey, scopenum): return items ignore = set() items_deque = deque(items) - items_done = OrderedDict() + items_done = order_preserving_dict() scoped_items_by_argkey = items_by_argkey[scopenum] scoped_argkeys_cache = argkeys_cache[scopenum] while items_deque: - no_argkey_group = OrderedDict() + no_argkey_group = order_preserving_dict() slicing_argkey = None while items_deque: item = items_deque.popleft() if item in items_done or item in no_argkey_group: continue - argkeys = OrderedDict.fromkeys( + argkeys = order_preserving_dict.fromkeys( k for k in scoped_argkeys_cache.get(item, []) if k not in ignore ) if not argkeys: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 9c88ca0ca06..08bc363547d 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -3,7 +3,6 @@ This is a good source for looking at the various reporting hooks. """ import argparse -import collections import datetime import inspect import platform @@ -28,6 +27,7 @@ import pytest from _pytest import nodes from _pytest._io import TerminalWriter +from _pytest.compat import order_preserving_dict from _pytest.config import Config from _pytest.config import ExitCode from _pytest.deprecated import TERMINALWRITER_WRITER @@ -839,8 +839,8 @@ def summary_warnings(self): return reports_grouped_by_message = ( - collections.OrderedDict() - ) # type: collections.OrderedDict[str, List[WarningReport]] + order_preserving_dict() + ) # type: Dict[str, List[WarningReport]] for wr in warning_reports: reports_grouped_by_message.setdefault(wr.message, []).append(wr) @@ -854,9 +854,7 @@ def collapsed_location_report(reports: List[WarningReport]): if len(locations) < 10: return "\n".join(map(str, locations)) - counts_by_filename = ( - collections.OrderedDict() - ) # type: collections.OrderedDict[str, int] + counts_by_filename = order_preserving_dict() # type: Dict[str, int] for loc in locations: key = str(loc).split("::", 1)[0] counts_by_filename[key] = counts_by_filename.get(key, 0) + 1 From 5702c86f4c8e2d79c1bf1ba6ff40e8879f8903c8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 16:01:18 +0300 Subject: [PATCH 166/823] nodes: micro-optimize hash(node) Turns out it's called alot, and saving the function call makes it faster. --- src/_pytest/nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 03a4b1af8a2..0a1f89c7456 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -216,7 +216,7 @@ def nodeid(self): return self._nodeid def __hash__(self): - return hash(self.nodeid) + return hash(self._nodeid) def setup(self): pass From b90f34569f39106c3a62a6b6087e9f0d70508190 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 16:08:24 +0300 Subject: [PATCH 167/823] nodes: micro-optimize Node attribute access --- src/_pytest/nodes.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 0a1f89c7456..56c1d4404b2 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -91,6 +91,19 @@ class Node(metaclass=NodeMeta): """ base class for Collector and Item the test collection tree. Collector subclasses have children, Items are terminal nodes.""" + # Use __slots__ to make attribute access faster. + # Note that __dict__ is still available. + __slots__ = ( + "name", + "parent", + "config", + "session", + "fspath", + "_nodeid", + "_store", + "__dict__", + ) + def __init__( self, name: str, From 89eee90b5f3e112d295d1aeb546a37234ce91faf Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Apr 2020 20:13:26 +0300 Subject: [PATCH 168/823] python: optimize PythonCollector.collect --- src/_pytest/python.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e1bd62f0bb7..8186b9af171 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -365,15 +365,17 @@ def collect(self): # NB. we avoid random getattrs and peek in the __dict__ instead # (XXX originally introduced from a PyPy need, still true?) dicts = [getattr(self.obj, "__dict__", {})] - for basecls in inspect.getmro(self.obj.__class__): + for basecls in self.obj.__class__.__mro__: dicts.append(basecls.__dict__) - seen = {} + seen = set() values = [] for dic in dicts: + # Note: seems like the dict can change during iteration - + # be careful not to remove the list() without consideration. for name, obj in list(dic.items()): if name in seen: continue - seen[name] = True + seen.add(name) res = self._makeitem(name, obj) if res is None: continue From 65963d20668cda6dc9df8b5c4a4ad6aa3ca74148 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 6 May 2020 21:29:03 +0300 Subject: [PATCH 169/823] warnings: speed up work done in catch_warnings_for_item() When setting up the warnings capture, filter strings (with the general form `action:message:category:module:line`) are collected from the cmdline, ini and item and applied. This happens for every test and other cases. To apply a string it needs to be parsed into a tuple, and it turns out this is slow. Since we already vendor the parsing code from Python's warnings.py, we can speed it up by caching the result. After splitting the parsing part from the applying part, the parsing is pure and is straightforward to cache. An alternative is to parse ahead of time and reuse the result, however the caching solution turns out cleaner and more general in this case. On this benchmark: import pytest @pytest.mark.parametrize("x", range(5000)) def test_foo(x): pass Before: ============================ 5000 passed in 14.11s ============================= 14365646 function calls (13450775 primitive calls) in 14.536 seconds After: ============================ 5000 passed in 13.61s ============================= 13290372 function calls (12375498 primitive calls) in 14.034 seconds --- src/_pytest/warnings.py | 46 ++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 2a4d189d573..527bb03b001 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -1,34 +1,52 @@ +import re import sys import warnings from contextlib import contextmanager +from functools import lru_cache from typing import Generator +from typing import Tuple import pytest +from _pytest.compat import TYPE_CHECKING from _pytest.main import Session +if TYPE_CHECKING: + from typing_extensions import Type -def _setoption(wmod, arg): - """ - Copy of the warning._setoption function but does not escape arguments. + +@lru_cache(maxsize=50) +def _parse_filter( + arg: str, *, escape: bool +) -> "Tuple[str, str, Type[Warning], str, int]": + """Parse a warnings filter string. + + This is copied from warnings._setoption, but does not apply the filter, + only parses it, and makes the escaping optional. """ parts = arg.split(":") if len(parts) > 5: - raise wmod._OptionError("too many fields (max 5): {!r}".format(arg)) + raise warnings._OptionError("too many fields (max 5): {!r}".format(arg)) while len(parts) < 5: parts.append("") - action, message, category, module, lineno = [s.strip() for s in parts] - action = wmod._getaction(action) - category = wmod._getcategory(category) - if lineno: + action_, message, category_, module, lineno_ = [s.strip() for s in parts] + action = warnings._getaction(action_) # type: str # type: ignore[attr-defined] + category = warnings._getcategory( + category_ + ) # type: Type[Warning] # type: ignore[attr-defined] + if message and escape: + message = re.escape(message) + if module and escape: + module = re.escape(module) + r"\Z" + if lineno_: try: - lineno = int(lineno) + lineno = int(lineno_) if lineno < 0: raise ValueError except (ValueError, OverflowError): - raise wmod._OptionError("invalid lineno {!r}".format(lineno)) + raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) else: lineno = 0 - wmod.filterwarnings(action, message, category, module, lineno) + return (action, message, category, module, lineno) def pytest_addoption(parser): @@ -79,15 +97,15 @@ def catch_warnings_for_item(config, ihook, when, item): # filters should have this precedence: mark, cmdline options, ini # filters should be applied in the inverse order of precedence for arg in inifilters: - _setoption(warnings, arg) + warnings.filterwarnings(*_parse_filter(arg, escape=False)) for arg in cmdline_filters: - warnings._setoption(arg) + warnings.filterwarnings(*_parse_filter(arg, escape=True)) if item is not None: for mark in item.iter_markers(name="filterwarnings"): for arg in mark.args: - _setoption(warnings, arg) + warnings.filterwarnings(*_parse_filter(arg, escape=False)) yield From dad328bc8a6546cf77eabbf7d033c9683487c934 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 May 2020 05:26:55 -0700 Subject: [PATCH 170/823] Fix tests for python3.9 --- .github/workflows/main.yml | 11 +++++++++++ setup.cfg | 1 + testing/code/test_source.py | 5 +++-- tox.ini | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7228355831f..cd331f742f6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,6 +39,7 @@ jobs: "ubuntu-py37-pluggy", "ubuntu-py37-freeze", "ubuntu-py38", + "ubuntu-py39", "ubuntu-pypy3", "macos-py37", @@ -98,6 +99,10 @@ jobs: python: "3.8" os: ubuntu-latest tox_env: "py38-xdist" + - name: "ubuntu-py39" + python: "3.8" + os: ubuntu-latest + tox_env: "py39-xdist" - name: "ubuntu-pypy3" python: "pypy3" os: ubuntu-latest @@ -133,6 +138,12 @@ jobs: uses: actions/setup-python@v1 with: python-version: ${{ matrix.python }} + - name: install python3.9 + if: matrix.tox_env == 'py39-xdist' + run: | + sudo add-apt-repository ppa:deadsnakes/nightly + sudo apt-get update + sudo apt-get install -y --no-install-recommends python3.9-dev python3.9-distutils - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/setup.cfg b/setup.cfg index 708951da489..ae053021914 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 platforms = unix, linux, osx, cygwin, win32 [options] diff --git a/testing/code/test_source.py b/testing/code/test_source.py index cf09309744a..792b8d6b160 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -122,7 +122,8 @@ def test_syntaxerror_rerepresentation() -> None: assert ex is not None assert ex.value.lineno == 1 assert ex.value.offset in {5, 7} # cpython: 7, pypy3.6 7.1.1: 5 - assert ex.value.text == "xyz xyz\n" + assert ex.value.text + assert ex.value.text.rstrip("\n") == "xyz xyz" def test_isparseable() -> None: @@ -521,7 +522,7 @@ class A: class B: pass - B.__name__ = "B2" + B.__name__ = B.__qualname__ = "B2" assert getfslineno(B)[1] == -1 co = compile("...", "", "eval") diff --git a/tox.ini b/tox.ini index 8f23e3cf918..f363f57012e 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ envlist = py36 py37 py38 + py39 pypy3 py37-{pexpect,xdist,unittestextras,numpy,pluggymaster} doctesting From de556f895febd89d14db0a0828e5c8555c75f44e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 7 May 2020 20:42:04 +0300 Subject: [PATCH 171/823] testing: clean up parametrization in test_mark.py (#7184) --- testing/test_mark.py | 85 +++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/testing/test_mark.py b/testing/test_mark.py index 30a18b38e7d..1c983b5af21 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -197,17 +197,17 @@ def test_hello(): @pytest.mark.parametrize( - "spec", + ("expr", "expected_passed"), [ - ("xyz", ("test_one",)), - ("((( xyz)) )", ("test_one",)), - ("not not xyz", ("test_one",)), - ("xyz and xyz2", ()), - ("xyz2", ("test_two",)), - ("xyz or xyz2", ("test_one", "test_two")), + ("xyz", ["test_one"]), + ("((( xyz)) )", ["test_one"]), + ("not not xyz", ["test_one"]), + ("xyz and xyz2", []), + ("xyz2", ["test_two"]), + ("xyz or xyz2", ["test_one", "test_two"]), ], ) -def test_mark_option(spec, testdir): +def test_mark_option(expr: str, expected_passed: str, testdir) -> None: testdir.makepyfile( """ import pytest @@ -219,18 +219,17 @@ def test_two(): pass """ ) - opt, passed_result = spec - rec = testdir.inline_run("-m", opt) + rec = testdir.inline_run("-m", expr) passed, skipped, fail = rec.listoutcomes() passed = [x.nodeid.split("::")[-1] for x in passed] - assert len(passed) == len(passed_result) - assert list(passed) == list(passed_result) + assert passed == expected_passed @pytest.mark.parametrize( - "spec", [("interface", ("test_interface",)), ("not interface", ("test_nointer",))] + ("expr", "expected_passed"), + [("interface", ["test_interface"]), ("not interface", ["test_nointer"])], ) -def test_mark_option_custom(spec, testdir): +def test_mark_option_custom(expr: str, expected_passed: str, testdir) -> None: testdir.makeconftest( """ import pytest @@ -248,26 +247,25 @@ def test_nointer(): pass """ ) - opt, passed_result = spec - rec = testdir.inline_run("-m", opt) + rec = testdir.inline_run("-m", expr) passed, skipped, fail = rec.listoutcomes() passed = [x.nodeid.split("::")[-1] for x in passed] - assert len(passed) == len(passed_result) - assert list(passed) == list(passed_result) + assert passed == expected_passed @pytest.mark.parametrize( - "spec", + ("expr", "expected_passed"), [ - ("interface", ("test_interface",)), - ("not interface", ("test_nointer", "test_pass", "test_1", "test_2")), - ("pass", ("test_pass",)), - ("not pass", ("test_interface", "test_nointer", "test_1", "test_2")), - ("not not not (pass)", ("test_interface", "test_nointer", "test_1", "test_2")), - ("1 or 2", ("test_1", "test_2")), + ("interface", ["test_interface"]), + ("not interface", ["test_nointer", "test_pass", "test_1", "test_2"]), + ("pass", ["test_pass"]), + ("not pass", ["test_interface", "test_nointer", "test_1", "test_2"]), + ("not not not (pass)", ["test_interface", "test_nointer", "test_1", "test_2"]), + ("1 or 2", ["test_1", "test_2"]), + ("not (1 or 2)", ["test_interface", "test_nointer", "test_pass"]), ], ) -def test_keyword_option_custom(spec, testdir): +def test_keyword_option_custom(expr: str, expected_passed: str, testdir) -> None: testdir.makepyfile( """ def test_interface(): @@ -282,12 +280,10 @@ def test_2(): pass """ ) - opt, passed_result = spec - rec = testdir.inline_run("-k", opt) + rec = testdir.inline_run("-k", expr) passed, skipped, fail = rec.listoutcomes() passed = [x.nodeid.split("::")[-1] for x in passed] - assert len(passed) == len(passed_result) - assert list(passed) == list(passed_result) + assert passed == expected_passed def test_keyword_option_considers_mark(testdir): @@ -298,14 +294,14 @@ def test_keyword_option_considers_mark(testdir): @pytest.mark.parametrize( - "spec", + ("expr", "expected_passed"), [ - ("None", ("test_func[None]",)), - ("[1.3]", ("test_func[1.3]",)), - ("2-3", ("test_func[2-3]",)), + ("None", ["test_func[None]"]), + ("[1.3]", ["test_func[1.3]"]), + ("2-3", ["test_func[2-3]"]), ], ) -def test_keyword_option_parametrize(spec, testdir): +def test_keyword_option_parametrize(expr: str, expected_passed: str, testdir) -> None: testdir.makepyfile( """ import pytest @@ -314,12 +310,10 @@ def test_func(arg): pass """ ) - opt, passed_result = spec - rec = testdir.inline_run("-k", opt) + rec = testdir.inline_run("-k", expr) passed, skipped, fail = rec.listoutcomes() passed = [x.nodeid.split("::")[-1] for x in passed] - assert len(passed) == len(passed_result) - assert list(passed) == list(passed_result) + assert passed == expected_passed def test_parametrize_with_module(testdir): @@ -338,7 +332,7 @@ def test_func(arg): @pytest.mark.parametrize( - "spec", + ("expr", "expected_error"), [ ( "foo or", @@ -360,17 +354,18 @@ def test_func(arg): ), ], ) -def test_keyword_option_wrong_arguments(spec, testdir, capsys): +def test_keyword_option_wrong_arguments( + expr: str, expected_error: str, testdir, capsys +) -> None: testdir.makepyfile( """ def test_func(arg): pass """ ) - opt, expected_result = spec - testdir.inline_run("-k", opt) - out = capsys.readouterr().err - assert expected_result in out + testdir.inline_run("-k", expr) + err = capsys.readouterr().err + assert expected_error in err def test_parametrized_collected_from_command_line(testdir): From b238845d0f4935f805521b09073211e50251f0dc Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 May 2020 13:14:58 -0700 Subject: [PATCH 172/823] Fix _is_setup_py for files encoded differently than locale --- changelog/7180.bugfix.rst | 1 + src/_pytest/doctest.py | 10 +++++----- testing/test_doctest.py | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 changelog/7180.bugfix.rst diff --git a/changelog/7180.bugfix.rst b/changelog/7180.bugfix.rst new file mode 100644 index 00000000000..d2dd55e9b83 --- /dev/null +++ b/changelog/7180.bugfix.rst @@ -0,0 +1 @@ +Fix ``_is_setup_py`` for files encoded differently than locale. diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 02108477818..e1dd9691cc9 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -108,20 +108,20 @@ def pytest_unconfigure(): RUNNER_CLASS = None -def pytest_collect_file(path, parent): +def pytest_collect_file(path: py.path.local, parent): config = parent.config if path.ext == ".py": - if config.option.doctestmodules and not _is_setup_py(config, path, parent): + if config.option.doctestmodules and not _is_setup_py(path): return DoctestModule.from_parent(parent, fspath=path) elif _is_doctest(config, path, parent): return DoctestTextfile.from_parent(parent, fspath=path) -def _is_setup_py(config, path, parent): +def _is_setup_py(path: py.path.local) -> bool: if path.basename != "setup.py": return False - contents = path.read() - return "setuptools" in contents or "distutils" in contents + contents = path.read_binary() + return b"setuptools" in contents or b"distutils" in contents def _is_doctest(config, path, parent): diff --git a/testing/test_doctest.py b/testing/test_doctest.py index c9defec5d5b..39afb4e9899 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -5,6 +5,7 @@ from _pytest.compat import MODULE_NOT_FOUND_ERROR from _pytest.doctest import _get_checker from _pytest.doctest import _is_mocked +from _pytest.doctest import _is_setup_py from _pytest.doctest import _patch_unwrap_mock_aware from _pytest.doctest import DoctestItem from _pytest.doctest import DoctestModule @@ -1487,3 +1488,27 @@ def test_warning_on_unwrap_of_broken_object(stop): with pytest.raises(KeyError): inspect.unwrap(bad_instance, stop=stop) assert inspect.unwrap.__module__ == "inspect" + + +def test_is_setup_py_not_named_setup_py(tmpdir): + not_setup_py = tmpdir.join("not_setup.py") + not_setup_py.write('from setuptools import setup; setup(name="foo")') + assert not _is_setup_py(not_setup_py) + + +@pytest.mark.parametrize("mod", ("setuptools", "distutils.core")) +def test_is_setup_py_is_a_setup_py(tmpdir, mod): + setup_py = tmpdir.join("setup.py") + setup_py.write('from {} import setup; setup(name="foo")'.format(mod)) + assert _is_setup_py(setup_py) + + +@pytest.mark.parametrize("mod", ("setuptools", "distutils.core")) +def test_is_setup_py_different_encoding(tmpdir, mod): + setup_py = tmpdir.join("setup.py") + contents = ( + "# -*- coding: cp1252 -*-\n" + 'from {} import setup; setup(name="foo", description="€")\n'.format(mod) + ) + setup_py.write_binary(contents.encode("cp1252")) + assert _is_setup_py(setup_py) From 9926fcf452db4a1e5af050de25c5f82ab3479061 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 7 May 2020 13:51:20 -0700 Subject: [PATCH 173/823] remove incorrect note about requiring admin install --- CONTRIBUTING.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d5137d9787a..ea424f1e6e7 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -212,9 +212,7 @@ Here is a simple overview, with pytest-specific bits: If you need some help with Git, follow this quick start guide: https://git.wiki.kernel.org/index.php/QuickStart -#. Install `pre-commit `_ and its hook on the pytest repo: - - **Note: pre-commit must be installed as admin, as it will not function otherwise**:: +#. Install `pre-commit `_ and its hook on the pytest repo:: $ pip install --user pre-commit $ pre-commit install From 73448f265d8c680c35dc66dfe65c3cc14fc337eb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 7 May 2020 21:20:09 +0300 Subject: [PATCH 174/823] Handle EPIPE/BrokenPipeError in pytest's CLI Running `pytest | head -1` and similar causes an annoying error to be printed to stderr: Exception ignored in: <_io.TextIOWrapper name='' mode='w' encoding='utf-8'> BrokenPipeError: [Errno 32] Broken pipe (or possibly even a propagating exception in older/other Python versions). The standard UNIX behavior is to handle the EPIPE silently. To recommended method to do this in Python is described here: https://docs.python.org/3/library/signal.html#note-on-sigpipe It is not appropriate to apply this recommendation to `pytest.main()`, which is used programmatically for in-process runs. Hence, change pytest's entrypoint to a new `pytest.console_main()` function, to be used exclusively by pytest's CLI, and add the SIGPIPE code there. Fixes #4375. --- changelog/4375.improvement.rst | 3 +++ setup.cfg | 4 ++-- src/_pytest/config/__init__.py | 18 ++++++++++++++++++ src/pytest/__init__.py | 2 ++ src/pytest/__main__.py | 2 +- testing/acceptance_test.py | 19 +++++++++++++++++++ 6 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 changelog/4375.improvement.rst diff --git a/changelog/4375.improvement.rst b/changelog/4375.improvement.rst new file mode 100644 index 00000000000..0c9a7f3e67a --- /dev/null +++ b/changelog/4375.improvement.rst @@ -0,0 +1,3 @@ +The ``pytest`` command now supresses the ``BrokenPipeError`` error message that +is printed to stderr when the output of ``pytest`` is piped and and the pipe is +closed by the piped-to program (common examples are ``less`` and ``head``). diff --git a/setup.cfg b/setup.cfg index 708951da489..5928781c074 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,8 +43,8 @@ python_requires = >=3.5 [options.entry_points] console_scripts = - pytest=pytest:main - py.test=pytest:main + pytest=pytest:console_main + py.test=pytest:console_main [build_sphinx] source-dir = doc/en/ diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 8e5944fc701..8a2c16d5d5e 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -137,6 +137,24 @@ def main(args=None, plugins=None) -> Union[int, ExitCode]: return ExitCode.USAGE_ERROR +def console_main() -> int: + """pytest's CLI entry point. + + This function is not meant for programmable use; use `main()` instead. + """ + # https://docs.python.org/3/library/signal.html#note-on-sigpipe + try: + code = main() + sys.stdout.flush() + return code + except BrokenPipeError: + # Python flushes standard streams on exit; redirect remaining output + # to devnull to avoid another BrokenPipeError at shutdown + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, sys.stdout.fileno()) + return 1 # Python exits with error code 1 on EPIPE + + class cmdline: # compatibility namespace main = staticmethod(main) diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 8629569b2f6..64d6d1f23ee 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -6,6 +6,7 @@ from _pytest import __version__ from _pytest.assertion import register_assert_rewrite from _pytest.config import cmdline +from _pytest.config import console_main from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config import hookspec @@ -57,6 +58,7 @@ "cmdline", "collect", "Collector", + "console_main", "deprecated_call", "exit", "ExitCode", diff --git a/src/pytest/__main__.py b/src/pytest/__main__.py index 01b2f6ccfe9..25b1e45b89d 100644 --- a/src/pytest/__main__.py +++ b/src/pytest/__main__.py @@ -4,4 +4,4 @@ import pytest if __name__ == "__main__": - raise SystemExit(pytest.main()) + raise SystemExit(pytest.console_main()) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 36a24a38a14..45a23ee935d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -11,6 +11,7 @@ from _pytest.compat import importlib_metadata from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Testdir def prepend_pythonpath(*dirs): @@ -1343,3 +1344,21 @@ def test_simple(): fullXml = f.read() assert "@this is stdout@\n" in fullXml assert "@this is stderr@\n" in fullXml + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="Windows raises `OSError: [Errno 22] Invalid argument` instead", +) +def test_no_brokenpipeerror_message(testdir: Testdir) -> None: + """Ensure that the broken pipe error message is supressed. + + In some Python versions, it reaches sys.unraisablehook, in others + a BrokenPipeError exception is propagated, but either way it prints + to stderr on shutdown, so checking nothing is printed is enough. + """ + popen = testdir.popen((*testdir._getpytestargs(), "--help")) + popen.stdout.close() + ret = popen.wait() + assert popen.stderr.read() == b"" + assert ret == 1 From c66bf59fd53a9f64fa7514b97395fca38e45f897 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 8 May 2020 13:03:51 +0300 Subject: [PATCH 175/823] ci: update github action versions, remove outdated comment (#7177) --- .github/workflows/main.yml | 18 ++++++++---------- .github/workflows/release-on-comment.yml | 7 +++++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cd331f742f6..45e386e5702 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,3 @@ -# evaluating GitHub actions for CI, disregard failures when evaluating PRs -# -# this is still missing: -# - deploy -# - upload github notes -# name: main on: @@ -133,9 +127,11 @@ jobs: use_coverage: true steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 + # For setuptools-scm. + - run: git fetch --prune --unshallow - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: install python3.9 @@ -180,9 +176,11 @@ jobs: needs: [build] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 + # For setuptools-scm. + - run: git fetch --prune --unshallow - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: "3.7" - name: Install dependencies diff --git a/.github/workflows/release-on-comment.yml b/.github/workflows/release-on-comment.yml index fe62eb1cb4b..9d803cd38dd 100644 --- a/.github/workflows/release-on-comment.yml +++ b/.github/workflows/release-on-comment.yml @@ -14,9 +14,12 @@ jobs: if: (github.event.comment && startsWith(github.event.comment.body, '@pytestbot please')) || (github.event.issue && !github.event.comment && startsWith(github.event.issue.body, '@pytestbot please')) steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 + # For setuptools-scm. + - run: git fetch --prune --unshallow + - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: "3.8" - name: Install dependencies From e6151cd8d0130cf91fe1fd888d1c571e6d561ccf Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 8 May 2020 09:03:21 -0300 Subject: [PATCH 176/823] Cherry-pick release 5.4.2 --- doc/en/announce/index.rst | 1 + doc/en/announce/release-5.4.2.rst | 22 +++++++++++++++++++ doc/en/changelog.rst | 36 +++++++++++++++++++++++++++++++ doc/en/writing_plugins.rst | 4 ++-- 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 doc/en/announce/release-5.4.2.rst diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 56e3172dd65..eeea782743d 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.4.2 release-5.4.1 release-5.4.0 release-5.3.5 diff --git a/doc/en/announce/release-5.4.2.rst b/doc/en/announce/release-5.4.2.rst new file mode 100644 index 00000000000..233faf127b5 --- /dev/null +++ b/doc/en/announce/release-5.4.2.rst @@ -0,0 +1,22 @@ +pytest-5.4.2 +======================================= + +pytest 5.4.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Ran Benita +* Ronny Pfannschmidt + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 00ed5497504..705bb10445b 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,42 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.4.2 (2020-05-08) +========================= + +Bug Fixes +--------- + +- `#6871 `_: Fix crash with captured output when using the :fixture:`capsysbinary fixture `. + + +- `#6924 `_: Ensure a ``unittest.IsolatedAsyncioTestCase`` is actually awaited. + + +- `#6925 `_: Fix TerminalRepr instances to be hashable again. + + +- `#6947 `_: Fix regression where functions registered with ``TestCase.addCleanup`` were not being called on test failures. + + +- `#6951 `_: Allow users to still set the deprecated ``TerminalReporter.writer`` attribute. + + +- `#6992 `_: Revert "tmpdir: clean up indirection via config for factories" #6767 as it breaks pytest-xdist. + + +- `#7110 `_: Fixed regression: ``asyncbase.TestCase`` tests are executed correctly again. + + +- `#7143 `_: Fix ``File.from_constructor`` so it forwards extra keyword arguments to the constructor. + + +- `#7145 `_: Classes with broken ``__getattribute__`` methods are displayed correctly during failures. + + +- `#7180 `_: Fix ``_is_setup_py`` for files encoded differently than locale. + + pytest 5.4.1 (2020-03-13) ========================= diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 87d81edb09f..5ee634d575f 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -443,9 +443,9 @@ additionally it is possible to copy examples for an example folder before runnin testdir.copy_example("test_example.py") test_example.py::test_plugin - $PYTHON_PREFIX/lib/python3.8/site-packages/_pytest/terminal.py:287: PytestDeprecationWarning: TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk. + $PYTHON_PREFIX/lib/python3.8/site-packages/_pytest/compat.py:333: PytestDeprecationWarning: The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk. See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information. - warnings.warn( + return getattr(object, name, default) -- Docs: https://docs.pytest.org/en/latest/warnings.html ====================== 2 passed, 2 warnings in 0.12s ======================= From 97a0239aadf8ac8760bc5dc7b0a9281ed84c02c8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 8 May 2020 12:36:40 -0300 Subject: [PATCH 177/823] Add myself to TIDELIFT --- TIDELIFT.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TIDELIFT.rst b/TIDELIFT.rst index 45247c29cf6..5ed2e31e90d 100644 --- a/TIDELIFT.rst +++ b/TIDELIFT.rst @@ -23,7 +23,7 @@ members of the `contributors team`_ interested in receiving funding. The current list of contributors receiving funding are: -* +* `@nicoddemus`_ Contributors interested in receiving a part of the funds just need to submit a PR adding their name to the list. Contributors that want to stop receiving the funds should also submit a PR From abb047f71b46ff162b92a4759edba620f1fada0e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 May 2020 08:57:22 -0700 Subject: [PATCH 178/823] Add asottile as well (went for alphabetical order, hope that's ok!) --- TIDELIFT.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TIDELIFT.rst b/TIDELIFT.rst index 5ed2e31e90d..b18f4793f81 100644 --- a/TIDELIFT.rst +++ b/TIDELIFT.rst @@ -23,6 +23,7 @@ members of the `contributors team`_ interested in receiving funding. The current list of contributors receiving funding are: +* `@asottile`_ * `@nicoddemus`_ Contributors interested in receiving a part of the funds just need to submit a PR adding their @@ -53,4 +54,5 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the .. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members .. _`agreement`: https://tidelift.com/docs/lifting/agreement +.. _`@asottile`: https://github.com/asottile .. _`@nicoddemus`: https://github.com/nicoddemus From 903e2ab6eef5a99625f4ef58115c9455b018feda Mon Sep 17 00:00:00 2001 From: Pavel Karateev Date: Sat, 9 May 2020 13:57:17 +0300 Subject: [PATCH 179/823] Fix #7126 - saferepr for bytes params bytes parametrize parameters cause error when --setup-show is used and Python is called with -bb flag --- AUTHORS | 1 + changelog/7126.bugfix.rst | 3 +++ src/_pytest/setuponly.py | 7 ++++++- testing/test_setuponly.py | 18 ++++++++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelog/7126.bugfix.rst diff --git a/AUTHORS b/AUTHORS index b59ebc2a27f..68f156862c5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -216,6 +216,7 @@ Ondřej Súkup Oscar Benjamin Patrick Hayes Pauli Virtanen +Pavel Karateev Paweł Adamczak Pedro Algarvio Philipp Loose diff --git a/changelog/7126.bugfix.rst b/changelog/7126.bugfix.rst new file mode 100644 index 00000000000..6817ee21192 --- /dev/null +++ b/changelog/7126.bugfix.rst @@ -0,0 +1,3 @@ +Use ``saferepr`` to format bytes ``parametrize`` parameters for ``--setup-show`` +output to prevent errors when Python is called with ``-bb`` to catch bytearray with +unicode comparison. diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index c9cc589ffee..e18208e59a9 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -1,4 +1,5 @@ import pytest +from _pytest._io.saferepr import saferepr def pytest_addoption(parser): @@ -66,7 +67,11 @@ def _show_fixture_action(fixturedef, msg): tw.write(" (fixtures used: {})".format(", ".join(deps))) if hasattr(fixturedef, "cached_param"): - tw.write("[{}]".format(fixturedef.cached_param)) + if isinstance(fixturedef.cached_param, bytes): + param = saferepr(fixturedef.cached_param, maxsize=42) + else: + param = fixturedef.cached_param + tw.write("[{}]".format(param)) tw.flush() diff --git a/testing/test_setuponly.py b/testing/test_setuponly.py index e26a33dee38..779c17fbd23 100644 --- a/testing/test_setuponly.py +++ b/testing/test_setuponly.py @@ -1,3 +1,5 @@ +import sys + import pytest from _pytest.config import ExitCode @@ -292,3 +294,19 @@ def test_arg(arg): ] ) assert result.ret == ExitCode.INTERRUPTED + + +def test_parametrize_no_comparing_bytearray_error(testdir): + test_file = testdir.makepyfile( + """ + import pytest + + @pytest.mark.parametrize('data', [b'Hello World']) + def test_data(data): + pass + """ + ) + result = testdir.run( + sys.executable, "-bb", "-m", "pytest", "--setup-show", str(test_file) + ) + assert result.ret == 0 From a2280d39ec9ff030abcbaba8e246c269587b9bea Mon Sep 17 00:00:00 2001 From: Pavel Karateev Date: Sat, 9 May 2020 14:14:23 +0300 Subject: [PATCH 180/823] #7126, use past tense in changelog --- changelog/7126.bugfix.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog/7126.bugfix.rst b/changelog/7126.bugfix.rst index 6817ee21192..ad3368d77f1 100644 --- a/changelog/7126.bugfix.rst +++ b/changelog/7126.bugfix.rst @@ -1,3 +1,3 @@ -Use ``saferepr`` to format bytes ``parametrize`` parameters for ``--setup-show`` -output to prevent errors when Python is called with ``-bb`` to catch bytearray with -unicode comparison. +Switched to ``saferepr`` to format bytes ``parametrize`` parameters +for ``--setup-show`` output to prevent errors when Python is called with ``-bb`` +to catch bytearray with unicode comparison. From feb7a5f0d1589bdc82473537e9017ff832e684f3 Mon Sep 17 00:00:00 2001 From: Pavel Karateev Date: Sun, 10 May 2020 12:11:59 +0300 Subject: [PATCH 181/823] Omit internal solution details --- changelog/7126.bugfix.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/changelog/7126.bugfix.rst b/changelog/7126.bugfix.rst index ad3368d77f1..a547688cb46 100644 --- a/changelog/7126.bugfix.rst +++ b/changelog/7126.bugfix.rst @@ -1,3 +1,2 @@ -Switched to ``saferepr`` to format bytes ``parametrize`` parameters -for ``--setup-show`` output to prevent errors when Python is called with ``-bb`` -to catch bytearray with unicode comparison. +``--setup-show`` now doesn't raise an error if bytearray is used as ``parametrize`` +parameter when Python is called with ``-bb`` flag. From 6b26f0f890f161f91d81b443468f10de1d5046a2 Mon Sep 17 00:00:00 2001 From: Pavel Karateev Date: Sun, 10 May 2020 12:19:52 +0300 Subject: [PATCH 182/823] Rename test method and reference issue --- testing/test_setuponly.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_setuponly.py b/testing/test_setuponly.py index 779c17fbd23..57c95ae06ae 100644 --- a/testing/test_setuponly.py +++ b/testing/test_setuponly.py @@ -296,7 +296,8 @@ def test_arg(arg): assert result.ret == ExitCode.INTERRUPTED -def test_parametrize_no_comparing_bytearray_error(testdir): +def test_show_fixture_action_with_bytearrays(testdir): + # Issue 7126, BytesWarning when using --setup-show with bytes parameter test_file = testdir.makepyfile( """ import pytest From 7b196747ddb55dd484ad5dfa8276d2d65cd737d4 Mon Sep 17 00:00:00 2001 From: Pavel Karateev Date: Sun, 10 May 2020 12:47:26 +0300 Subject: [PATCH 183/823] Use saferepr for all types --- src/_pytest/setuponly.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index e18208e59a9..9e4cd951947 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -67,11 +67,7 @@ def _show_fixture_action(fixturedef, msg): tw.write(" (fixtures used: {})".format(", ".join(deps))) if hasattr(fixturedef, "cached_param"): - if isinstance(fixturedef.cached_param, bytes): - param = saferepr(fixturedef.cached_param, maxsize=42) - else: - param = fixturedef.cached_param - tw.write("[{}]".format(param)) + tw.write("[{}]".format(saferepr(fixturedef.cached_param, maxsize=42))) tw.flush() From 184528d0c28f2cde73a0457414c55218fd6a2ddf Mon Sep 17 00:00:00 2001 From: Pavel Karateev Date: Sun, 10 May 2020 13:06:36 +0300 Subject: [PATCH 184/823] Fix tests to expected repr output --- testing/test_setuponly.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/testing/test_setuponly.py b/testing/test_setuponly.py index 57c95ae06ae..9b0bb909bf1 100644 --- a/testing/test_setuponly.py +++ b/testing/test_setuponly.py @@ -148,10 +148,10 @@ def test_arg1(arg_other): result.stdout.fnmatch_lines( [ - "SETUP S arg_same?foo?", - "TEARDOWN S arg_same?foo?", - "SETUP S arg_same?bar?", - "TEARDOWN S arg_same?bar?", + "SETUP S arg_same?'foo'?", + "TEARDOWN S arg_same?'foo'?", + "SETUP S arg_same?'bar'?", + "TEARDOWN S arg_same?'bar'?", ] ) @@ -181,7 +181,7 @@ def test_arg1(arg_other): assert result.ret == 0 result.stdout.fnmatch_lines( - ["SETUP S arg_same?spam?", "SETUP S arg_same?ham?"] + ["SETUP S arg_same?'spam'?", "SETUP S arg_same?'ham'?"] ) @@ -200,7 +200,9 @@ def test_foobar(foobar): result = testdir.runpytest(mode, p) assert result.ret == 0 - result.stdout.fnmatch_lines(["*SETUP F foobar?FOO?", "*SETUP F foobar?BAR?"]) + result.stdout.fnmatch_lines( + ["*SETUP F foobar?'FOO'?", "*SETUP F foobar?'BAR'?"] + ) def test_dynamic_fixture_request(testdir): From 17857b67df4c4cf1ba7def1d896426e583e4120c Mon Sep 17 00:00:00 2001 From: Pavel Karateev Date: Sun, 10 May 2020 16:58:22 +0300 Subject: [PATCH 185/823] Better changelog wording Co-authored-by: Ran Benita --- changelog/7126.bugfix.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/7126.bugfix.rst b/changelog/7126.bugfix.rst index a547688cb46..1a85e899781 100644 --- a/changelog/7126.bugfix.rst +++ b/changelog/7126.bugfix.rst @@ -1,2 +1,2 @@ -``--setup-show`` now doesn't raise an error if bytearray is used as ``parametrize`` -parameter when Python is called with ``-bb`` flag. +``--setup-show`` now doesn't raise an error when a bytes value is used as a ``parametrize`` +parameter when Python is called with the ``-bb`` flag. From 8bd3f1a72bf794e919f0fe2d06b426729f53b092 Mon Sep 17 00:00:00 2001 From: Pavel Karateev Date: Sun, 10 May 2020 16:59:20 +0300 Subject: [PATCH 186/823] Better test method name Co-authored-by: Ran Benita --- testing/test_setuponly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_setuponly.py b/testing/test_setuponly.py index 9b0bb909bf1..221c32a310b 100644 --- a/testing/test_setuponly.py +++ b/testing/test_setuponly.py @@ -298,7 +298,7 @@ def test_arg(arg): assert result.ret == ExitCode.INTERRUPTED -def test_show_fixture_action_with_bytearrays(testdir): +def test_show_fixture_action_with_bytes(testdir): # Issue 7126, BytesWarning when using --setup-show with bytes parameter test_file = testdir.makepyfile( """ From dbfc629698b66162238ae7fc4e4f771843e2bdea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Wilczy=C5=84ski?= Date: Mon, 11 May 2020 13:26:16 +0200 Subject: [PATCH 187/823] #7138 Docs improvement: Apply indirect on particular arguments --- doc/en/example/parametrize.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index df558d1bae6..c9f430de944 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -389,16 +389,17 @@ The result of this test will be successful: .. code-block:: pytest - $ pytest test_indirect_list.py --collect-only + $ pytest -v test_indirect_list.py =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item - - - ========================== no tests ran in 0.12s =========================== + test_indirect_list.py::test_indirect[a-b] PASSED + + ========================== 1 passed in 0.01s =============================== + .. regendoc:wipe From c4f9eaa5de7db6dc23e4f8296f3504a8d684fca0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 11 May 2020 15:44:01 +0300 Subject: [PATCH 188/823] mark: deprecate a couple undocumented -k syntaxes The `-k '-expr'` syntax is an old alias to `-k 'not expr'`. It's also not a very convenient to have syntax that start with `-` on the CLI. Deprecate it and suggest replacing with `not`. --- The `-k 'expr:'` syntax discards all items until the first match and keeps all subsequent, e.g. `-k foo` with test_bar test_foo test_baz results in `test_foo`, `test_baz`. That's a bit weird, so deprecate it without a replacement. If someone complains we can reconsider or devise a better alternative. --- changelog/7210.deprecation.rst | 5 +++++ src/_pytest/deprecated.py | 10 ++++++++++ src/_pytest/mark/__init__.py | 7 +++++++ testing/deprecated_test.py | 24 ++++++++++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 changelog/7210.deprecation.rst diff --git a/changelog/7210.deprecation.rst b/changelog/7210.deprecation.rst new file mode 100644 index 00000000000..3e1350eaa79 --- /dev/null +++ b/changelog/7210.deprecation.rst @@ -0,0 +1,5 @@ +The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'`` +instead. + +The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue +if you use this and want a replacement. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 8c9bd9d5c42..9f4570f85b0 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -75,3 +75,13 @@ "The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.\n" "See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information." ) + + +MINUS_K_DASH = PytestDeprecationWarning( + "The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead." +) + +MINUS_K_COLON = PytestDeprecationWarning( + "The `-k 'expr:'` syntax to -k is deprecated.\n" + "Please open an issue if you use this and want a replacement." +) diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 36245c25a05..f7556b0b761 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -1,4 +1,5 @@ """ generic mechanism for marking and selecting python functions. """ +import warnings from typing import Optional from .legacy import matchkeyword @@ -13,6 +14,8 @@ from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config import UsageError +from _pytest.deprecated import MINUS_K_COLON +from _pytest.deprecated import MINUS_K_DASH from _pytest.store import StoreKey __all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"] @@ -107,9 +110,13 @@ def deselect_by_keyword(items, config): return if keywordexpr.startswith("-"): + # To be removed in pytest 7.0.0. + warnings.warn(MINUS_K_DASH, stacklevel=2) keywordexpr = "not " + keywordexpr[1:] selectuntil = False if keywordexpr[-1:] == ":": + # To be removed in pytest 7.0.0. + warnings.warn(MINUS_K_COLON, stacklevel=2) selectuntil = True keywordexpr = keywordexpr[:-1] diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 5ad055bd21e..edd5505c03d 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -164,3 +164,27 @@ def test__fillfuncargs_is_deprecated() -> None: match="The `_fillfuncargs` function is deprecated", ): pytest._fillfuncargs(mock.Mock()) + + +def test_minus_k_dash_is_deprecated(testdir) -> None: + threepass = testdir.makepyfile( + test_threepass=""" + def test_one(): assert 1 + def test_two(): assert 1 + def test_three(): assert 1 + """ + ) + result = testdir.runpytest("-k=-test_two", threepass) + result.stdout.fnmatch_lines(["*The `-k '-expr'` syntax*deprecated*"]) + + +def test_minus_k_colon_is_deprecated(testdir) -> None: + threepass = testdir.makepyfile( + test_threepass=""" + def test_one(): assert 1 + def test_two(): assert 1 + def test_three(): assert 1 + """ + ) + result = testdir.runpytest("-k", "test_two:", threepass) + result.stdout.fnmatch_lines(["*The `-k 'expr:'` syntax*deprecated*"]) From 06b6d5b1d58c59ddde05e40e063067c6322b650f Mon Sep 17 00:00:00 2001 From: Hunter Richards Date: Mon, 11 May 2020 15:36:08 -0500 Subject: [PATCH 189/823] Combine all mentions of `yield` into "Unsupported" section --- doc/en/nose.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/doc/en/nose.rst b/doc/en/nose.rst index 2939d91e5a9..1ac70af6c6b 100644 --- a/doc/en/nose.rst +++ b/doc/en/nose.rst @@ -26,7 +26,6 @@ Supported nose Idioms * setup and teardown at module/class/method level * SkipTest exceptions and markers * setup/teardown decorators -* ``yield``-based tests and their setup (considered deprecated as of pytest 3.0) * ``__test__`` attribute on modules/classes/functions * general usage of nose utilities @@ -65,10 +64,8 @@ Unsupported idioms / known issues - no nose-configuration is recognized. -- ``yield``-based methods don't support ``setup`` properly because - the ``setup`` method is always called in the same class instance. - There are no plans to fix this currently because ``yield``-tests - are deprecated in pytest 3.0, with ``pytest.mark.parametrize`` - being the recommended alternative. +- ``yield``-based methods are unsupported as of pytest 4.1.0. They are + fundamentally incompatible with pytest because they don't support fixtures + properly since collection and test execution are separated. .. _nose: https://nose.readthedocs.io/en/latest/ From d1534181c0bd165f354179d1ac131d874b71a81b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 24 Apr 2020 21:51:32 +0300 Subject: [PATCH 190/823] pre-commit: upgrade flake8 3.7.7 -> 3.8.1 New errors: testing/test_setupplan.py:104:15: E741 ambiguous variable name 'l' testing/test_setupplan.py:107:15: E741 ambiguous variable name 'l' extra/get_issues.py:48:29: E741 ambiguous variable name 'l' testing/test_error_diffs.py:270:32: E741 ambiguous variable name 'l' Not so sure about it but easier to just fix. But more importantly, is a large amount of typing-related issues there were fixed which necessitated noqa's which can now be removed. --- .pre-commit-config.yaml | 4 ++-- extra/get_issues.py | 2 +- testing/test_error_diffs.py | 2 +- testing/test_setupplan.py | 8 ++++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fd909dd5da5..81e30ecc1e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,11 +21,11 @@ repos: exclude: _pytest/debugging.py language_version: python3 - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.7 + rev: 3.8.1 hooks: - id: flake8 language_version: python3 - additional_dependencies: [flake8-typing-imports==1.3.0] + additional_dependencies: [flake8-typing-imports==1.9.0] - repo: https://github.com/asottile/reorder_python_imports rev: v1.4.0 hooks: diff --git a/extra/get_issues.py b/extra/get_issues.py index 9407aeded7d..c264b26446d 100644 --- a/extra/get_issues.py +++ b/extra/get_issues.py @@ -45,7 +45,7 @@ def main(args): def _get_kind(issue): - labels = [l["name"] for l in issue["labels"]] + labels = [label["name"] for label in issue["labels"]] for key in ("bug", "enhancement", "proposal"): if key in labels: return key diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index c7198bde00e..473c62a7571 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -267,7 +267,7 @@ def test_this(): @pytest.mark.parametrize("code, expected", TESTCASES) def test_error_diff(code, expected, testdir): - expected = [l.lstrip() for l in expected.splitlines()] + expected = [line.lstrip() for line in expected.splitlines()] p = testdir.makepyfile(code) result = testdir.runpytest(p, "-vv") result.stdout.fnmatch_lines(expected) diff --git a/testing/test_setupplan.py b/testing/test_setupplan.py index a44474dd155..64b464b32dd 100644 --- a/testing/test_setupplan.py +++ b/testing/test_setupplan.py @@ -101,10 +101,14 @@ def test_two(self, sess, mod, cls, func): # the number and text of these lines should be identical plan_lines = [ - l for l in plan_result.stdout.lines if "SETUP" in l or "TEARDOWN" in l + line + for line in plan_result.stdout.lines + if "SETUP" in line or "TEARDOWN" in line ] show_lines = [ - l for l in show_result.stdout.lines if "SETUP" in l or "TEARDOWN" in l + line + for line in show_result.stdout.lines + if "SETUP" in line or "TEARDOWN" in line ] assert plan_lines == show_lines From 59a12e9ab3990496940b79eba8dde6fcd481c8c3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 24 Apr 2020 22:15:45 +0300 Subject: [PATCH 191/823] Replace bare `except`s with `except BaseException` Mostly I wanted to remove uses of `noqa`. In Python 3 the two are the same. --- src/_pytest/_code/code.py | 6 +++--- src/_pytest/main.py | 2 +- src/_pytest/python_api.py | 2 +- src/_pytest/runner.py | 2 +- src/_pytest/unittest.py | 2 +- testing/code/test_excinfo.py | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 6102084f0b8..bc0e366933d 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -277,7 +277,7 @@ def __str__(self) -> str: line = str(self.statement).lstrip() except KeyboardInterrupt: raise - except: # noqa + except BaseException: line = "???" return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line) @@ -667,12 +667,12 @@ def _getindent(self, source: "Source") -> int: s = str(source.getstatement(len(source) - 1)) except KeyboardInterrupt: raise - except: # noqa + except BaseException: try: s = str(source[-1]) except KeyboardInterrupt: raise - except: # noqa + except BaseException: return 0 return 4 + (len(s) - len(s.lstrip())) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 61eb7ca74c2..de7e16744a4 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -206,7 +206,7 @@ def wrap_session( ) config.hook.pytest_keyboard_interrupt(excinfo=excinfo) session.exitstatus = exitstatus - except: # noqa + except BaseException: session.exitstatus = ExitCode.INTERNAL_ERROR excinfo = _pytest._code.ExceptionInfo.from_current() try: diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 7f52778b9b0..78051172d8f 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -123,7 +123,7 @@ def __eq__(self, actual): if not np.isscalar(actual): try: actual = np.asarray(actual) - except: # noqa + except BaseException: raise TypeError("cannot compare '{}' to numpy.ndarray".format(actual)) if not np.isscalar(actual) and actual.shape != self.expected.shape: diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 76785ada7f3..e7211369cc8 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -258,7 +258,7 @@ def from_call(cls, func, when, reraise=None) -> "CallInfo": precise_start = perf_counter() try: result = func() - except: # noqa + except BaseException: excinfo = ExceptionInfo.from_current() if reraise is not None and excinfo.errisinstance(reraise): raise diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index fc3d1a51533..773f545af2e 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -151,7 +151,7 @@ def _addexcinfo(self, rawexcinfo): fail("".join(values), pytrace=False) except (fail.Exception, KeyboardInterrupt): raise - except: # noqa + except BaseException: fail( "ERROR: Unknown Incompatible Exception " "representation:\n%r" % (rawexcinfo,), diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index f0c7146c7e5..08c0619e3ff 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -239,7 +239,7 @@ def reraise_me() -> None: def f(n: int) -> None: try: do_stuff() - except: # noqa + except BaseException: reraise_me() excinfo = pytest.raises(RuntimeError, f, 8) @@ -445,7 +445,7 @@ def excinfo_from_exec(self, source): exec(source.compile()) except KeyboardInterrupt: raise - except: # noqa + except BaseException: return _pytest._code.ExceptionInfo.from_current() assert 0, "did not raise" From 23c9856857b4fad351491b064a30a1e1eddc0589 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 24 Apr 2020 22:24:40 +0300 Subject: [PATCH 192/823] Remove no longer needed noqa's --- src/_pytest/_code/code.py | 2 +- src/_pytest/capture.py | 2 +- src/_pytest/compat.py | 2 +- src/_pytest/config/argparsing.py | 2 +- src/_pytest/config/findpaths.py | 2 +- src/_pytest/logging.py | 14 +++++++------- src/_pytest/mark/legacy.py | 2 +- src/_pytest/nodes.py | 2 +- src/_pytest/outcomes.py | 2 +- src/_pytest/python.py | 4 ++-- src/_pytest/python_api.py | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index bc0e366933d..2075fd0eb34 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -44,7 +44,7 @@ if TYPE_CHECKING: from typing import Type from typing_extensions import Literal - from weakref import ReferenceType # noqa: F401 + from weakref import ReferenceType _TracebackStyle = Literal["long", "short", "line", "no", "native"] diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index d34bf23f879..7eafeb3e406 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -669,7 +669,7 @@ def writeorg(self, data): class SysCapture(SysCaptureBinary): - EMPTY_BUFFER = "" # type: ignore[assignment] # noqa: F821 + EMPTY_BUFFER = "" # type: ignore[assignment] def snap(self): res = self.tmpfile.getvalue() diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 6a0614f0129..4cc22ba4a0d 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -32,7 +32,7 @@ if TYPE_CHECKING: - from typing import Type # noqa: F401 (used in type string) + from typing import Type _T = TypeVar("_T") diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 940eaa6a799..b57db92caac 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: from typing import NoReturn - from typing_extensions import Literal # noqa: F401 + from typing_extensions import Literal FILE_OR_DIR = "file_or_dir" diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 101fdf66fb4..f4f62e06b8f 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -12,7 +12,7 @@ from _pytest.outcomes import fail if TYPE_CHECKING: - from . import Config # noqa: F401 + from . import Config def exists(path, ignore=OSError): diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 5e60a232172..681fdee6213 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -338,7 +338,7 @@ def handler(self) -> LogCaptureHandler: """ :rtype: LogCaptureHandler """ - return self._item.catch_log_handler # type: ignore[no-any-return] # noqa: F723 + return self._item.catch_log_handler # type: ignore[no-any-return] def get_records(self, when: str) -> List[logging.LogRecord]: """ @@ -354,7 +354,7 @@ def get_records(self, when: str) -> List[logging.LogRecord]: """ handler = self._item.catch_log_handlers.get(when) if handler: - return handler.records # type: ignore[no-any-return] # noqa: F723 + return handler.records # type: ignore[no-any-return] else: return [] @@ -640,15 +640,15 @@ def _runtest_for_main( return if not hasattr(item, "catch_log_handlers"): - item.catch_log_handlers = {} # type: ignore[attr-defined] # noqa: F821 - item.catch_log_handlers[when] = log_handler # type: ignore[attr-defined] # noqa: F821 - item.catch_log_handler = log_handler # type: ignore[attr-defined] # noqa: F821 + item.catch_log_handlers = {} # type: ignore[attr-defined] + item.catch_log_handlers[when] = log_handler # type: ignore[attr-defined] + item.catch_log_handler = log_handler # type: ignore[attr-defined] try: yield # run test finally: if when == "teardown": - del item.catch_log_handler # type: ignore[attr-defined] # noqa: F821 - del item.catch_log_handlers # type: ignore[attr-defined] # noqa: F821 + del item.catch_log_handler # type: ignore[attr-defined] + del item.catch_log_handlers # type: ignore[attr-defined] if self.print_logs: # Add a captured log section to the report. diff --git a/src/_pytest/mark/legacy.py b/src/_pytest/mark/legacy.py index a9461d4cef2..1a9fdee8dfc 100644 --- a/src/_pytest/mark/legacy.py +++ b/src/_pytest/mark/legacy.py @@ -12,7 +12,7 @@ from _pytest.mark.expression import ParseError if TYPE_CHECKING: - from _pytest.nodes import Item # noqa: F401 (used in type string) + from _pytest.nodes import Item @attr.s diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index d55e000340a..448e6712797 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -33,7 +33,7 @@ if TYPE_CHECKING: # Imported here due to circular import. - from _pytest.main import Session # noqa: F401 + from _pytest.main import Session SEP = "/" diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index bed73c94de9..7d7e9df7af2 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from typing import NoReturn - from typing import Type # noqa: F401 (Used in string type annotation.) + from typing import Type # noqa: F401 (used in type string) from typing_extensions import Protocol else: # typing.Protocol is only available starting from Python 3.8. It is also diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ef3ebf79133..6ae57cb3718 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -136,7 +136,7 @@ def pytest_cmdline_main(config): def pytest_generate_tests(metafunc: "Metafunc") -> None: for marker in metafunc.definition.iter_markers(name="parametrize"): # TODO: Fix this type-ignore (overlapping kwargs). - metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) # type: ignore[misc] # noqa: F821 + metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) # type: ignore[misc] def pytest_configure(config): @@ -1013,7 +1013,7 @@ def _validate_ids( func_name: str, ) -> List[Union[None, str]]: try: - num_ids = len(ids) # type: ignore[arg-type] # noqa: F821 + num_ids = len(ids) # type: ignore[arg-type] except TypeError: try: iter(ids) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 78051172d8f..a523caae5ac 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -27,7 +27,7 @@ from _pytest.outcomes import fail if TYPE_CHECKING: - from typing import Type # noqa: F401 (used in type string) + from typing import Type BASE_TYPE = (type, STRING_TYPES) From 645aaa728d515558725eb4d9bf8414612dfe4513 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 12 May 2020 12:07:56 +0300 Subject: [PATCH 193/823] python_api: reduce scope of a `except BaseException` in ApproxNumpy I'm not sure if it can even raise at all, but catching BaseException would swallow ctrl-C and such and is definitely inappropriate here. --- src/_pytest/python_api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index a523caae5ac..29c8af7e281 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -123,8 +123,10 @@ def __eq__(self, actual): if not np.isscalar(actual): try: actual = np.asarray(actual) - except BaseException: - raise TypeError("cannot compare '{}' to numpy.ndarray".format(actual)) + except Exception as e: + raise TypeError( + "cannot compare '{}' to numpy.ndarray".format(actual) + ) from e if not np.isscalar(actual) and actual.shape != self.expected.shape: return False From 622c4ce02efb543027932d3d39c8db8b7fefe337 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 11 May 2020 11:20:43 +0300 Subject: [PATCH 194/823] mark/expression: support compiling once and reusing for multiple evaluations In current pytest, the same expression is matched against all items. But it is re-parsed for every match. Add support for "compiling" an expression and reusing the result. Errors may only occur during compilation. This is done by parsing the expression into a Python `ast.Expression`, then `compile()`ing it into a code object. Evaluation is then done using `eval()`. Note: historically we used to use `eval` directly on the user input -- this is not the case here, the expression is entirely under our control according to our grammar, we just JIT-compile it to Python as a (completely safe) optimization. --- src/_pytest/mark/expression.py | 95 ++++++++++++++++++++++++--------- src/_pytest/mark/legacy.py | 8 +-- testing/test_mark_expression.py | 8 ++- 3 files changed, 82 insertions(+), 29 deletions(-) diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 008192d4af2..04c73411af5 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -15,10 +15,13 @@ - ident evaluates to True of False according to a provided matcher function. - or/and/not evaluate according to the usual boolean semantics. """ +import ast import enum import re +import types from typing import Callable from typing import Iterator +from typing import Mapping from typing import Optional from typing import Sequence @@ -31,7 +34,7 @@ __all__ = [ - "evaluate", + "Expression", "ParseError", ] @@ -124,50 +127,92 @@ def reject(self, expected: Sequence[TokenType]) -> "NoReturn": ) -def expression(s: Scanner, matcher: Callable[[str], bool]) -> bool: +def expression(s: Scanner) -> ast.Expression: if s.accept(TokenType.EOF): - return False - ret = expr(s, matcher) - s.accept(TokenType.EOF, reject=True) - return ret + ret = ast.NameConstant(False) # type: ast.expr + else: + ret = expr(s) + s.accept(TokenType.EOF, reject=True) + return ast.fix_missing_locations(ast.Expression(ret)) -def expr(s: Scanner, matcher: Callable[[str], bool]) -> bool: - ret = and_expr(s, matcher) +def expr(s: Scanner) -> ast.expr: + ret = and_expr(s) while s.accept(TokenType.OR): - rhs = and_expr(s, matcher) - ret = ret or rhs + rhs = and_expr(s) + ret = ast.BoolOp(ast.Or(), [ret, rhs]) return ret -def and_expr(s: Scanner, matcher: Callable[[str], bool]) -> bool: - ret = not_expr(s, matcher) +def and_expr(s: Scanner) -> ast.expr: + ret = not_expr(s) while s.accept(TokenType.AND): - rhs = not_expr(s, matcher) - ret = ret and rhs + rhs = not_expr(s) + ret = ast.BoolOp(ast.And(), [ret, rhs]) return ret -def not_expr(s: Scanner, matcher: Callable[[str], bool]) -> bool: +def not_expr(s: Scanner) -> ast.expr: if s.accept(TokenType.NOT): - return not not_expr(s, matcher) + return ast.UnaryOp(ast.Not(), not_expr(s)) if s.accept(TokenType.LPAREN): - ret = expr(s, matcher) + ret = expr(s) s.accept(TokenType.RPAREN, reject=True) return ret ident = s.accept(TokenType.IDENT) if ident: - return matcher(ident.value) + return ast.Name(ident.value, ast.Load()) s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) -def evaluate(input: str, matcher: Callable[[str], bool]) -> bool: - """Evaluate a match expression as used by -k and -m. +class MatcherAdapter(Mapping[str, bool]): + """Adapts a matcher function to a locals mapping as required by eval().""" + + def __init__(self, matcher: Callable[[str], bool]) -> None: + self.matcher = matcher + + def __getitem__(self, key: str) -> bool: + return self.matcher(key) + + def __iter__(self) -> Iterator[str]: + raise NotImplementedError() - :param input: The input expression - one line. - :param matcher: Given an identifier, should return whether it matches or not. - Should be prepared to handle arbitrary strings as input. + def __len__(self) -> int: + raise NotImplementedError() - Returns whether the entire expression matches or not. + +class Expression: + """A compiled match expression as used by -k and -m. + + The expression can be evaulated against different matchers. """ - return expression(Scanner(input), matcher) + + __slots__ = ("code",) + + def __init__(self, code: types.CodeType) -> None: + self.code = code + + @classmethod + def compile(self, input: str) -> "Expression": + """Compile a match expression. + + :param input: The input expression - one line. + """ + astexpr = expression(Scanner(input)) + code = compile( + astexpr, filename="", mode="eval", + ) # type: types.CodeType + return Expression(code) + + def evaluate(self, matcher: Callable[[str], bool]) -> bool: + """Evaluate the match expression. + + :param matcher: Given an identifier, should return whether it matches or not. + Should be prepared to handle arbitrary strings as input. + + Returns whether the expression matches or not. + """ + ret = eval( + self.code, {"__builtins__": {}}, MatcherAdapter(matcher) + ) # type: bool + return ret diff --git a/src/_pytest/mark/legacy.py b/src/_pytest/mark/legacy.py index 1a9fdee8dfc..ed707fcc739 100644 --- a/src/_pytest/mark/legacy.py +++ b/src/_pytest/mark/legacy.py @@ -8,7 +8,7 @@ from _pytest.compat import TYPE_CHECKING from _pytest.config import UsageError -from _pytest.mark.expression import evaluate +from _pytest.mark.expression import Expression from _pytest.mark.expression import ParseError if TYPE_CHECKING: @@ -77,11 +77,12 @@ def __call__(self, subname: str) -> bool: def matchmark(colitem, markexpr: str) -> bool: """Tries to match on any marker names, attached to the given colitem.""" try: - return evaluate(markexpr, MarkMatcher.from_item(colitem)) + expression = Expression.compile(markexpr) except ParseError as e: raise UsageError( "Wrong expression passed to '-m': {}: {}".format(markexpr, e) ) from None + return expression.evaluate(MarkMatcher.from_item(colitem)) def matchkeyword(colitem, keywordexpr: str) -> bool: @@ -94,8 +95,9 @@ def matchkeyword(colitem, keywordexpr: str) -> bool: any item, as well as names directly assigned to test functions. """ try: - return evaluate(keywordexpr, KeywordMatcher.from_item(colitem)) + expression = Expression.compile(keywordexpr) except ParseError as e: raise UsageError( "Wrong expression passed to '-k': {}: {}".format(keywordexpr, e) ) from None + return expression.evaluate(KeywordMatcher.from_item(colitem)) diff --git a/testing/test_mark_expression.py b/testing/test_mark_expression.py index 74576786d13..335888618ad 100644 --- a/testing/test_mark_expression.py +++ b/testing/test_mark_expression.py @@ -1,8 +1,14 @@ +from typing import Callable + import pytest -from _pytest.mark.expression import evaluate +from _pytest.mark.expression import Expression from _pytest.mark.expression import ParseError +def evaluate(input: str, matcher: Callable[[str], bool]) -> bool: + return Expression.compile(input).evaluate(matcher) + + def test_empty_is_false() -> None: assert not evaluate("", lambda ident: False) assert not evaluate("", lambda ident: True) From c714f05ad707fae11804e34cf38bf5ba0fbf0b88 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 11 May 2020 11:50:41 +0300 Subject: [PATCH 195/823] mark: reuse compiled expression for all items in -k/-m The previous commit made this possible, so utilize it. Since legacy.py becomes pretty bare, I inlined it into __init__.py. I'm not sure it's really "legacy" anyway! Using a simple 50000 items benchmark with `--collect-only -k nomatch`: Before (two commits ago): ======================== 50000 deselected in 10.31s ===================== 19129345 function calls (18275596 primitive calls) in 10.634 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.001 0.001 2.270 2.270 __init__.py:149(pytest_collection_modifyitems) 1 0.036 0.036 2.270 2.270 __init__.py:104(deselect_by_keyword) 50000 0.055 0.000 2.226 0.000 legacy.py:87(matchkeyword) After: ======================== 50000 deselected in 9.37s ========================= 18029363 function calls (17175972 primitive calls) in 9.701 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 1.394 1.394 __init__.py:239(pytest_collection_modifyitems) 1 0.057 0.057 1.393 1.393 __init__.py:162(deselect_by_keyword) The matching itself can be optimized more but that's a different story. --- src/_pytest/mark/__init__.py | 98 +++++++++++++++++++++++++++++++-- src/_pytest/mark/legacy.py | 103 ----------------------------------- testing/test_pytester.py | 2 +- 3 files changed, 95 insertions(+), 108 deletions(-) delete mode 100644 src/_pytest/mark/legacy.py diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index f7556b0b761..134ed187641 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -1,9 +1,12 @@ """ generic mechanism for marking and selecting python functions. """ import warnings +from typing import AbstractSet from typing import Optional -from .legacy import matchkeyword -from .legacy import matchmark +import attr + +from .expression import Expression +from .expression import ParseError from .structures import EMPTY_PARAMETERSET_OPTION from .structures import get_empty_parameterset_mark from .structures import Mark @@ -11,6 +14,7 @@ from .structures import MarkDecorator from .structures import MarkGenerator from .structures import ParameterSet +from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config import UsageError @@ -18,6 +22,9 @@ from _pytest.deprecated import MINUS_K_DASH from _pytest.store import StoreKey +if TYPE_CHECKING: + from _pytest.nodes import Item + __all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"] @@ -104,6 +111,57 @@ def pytest_cmdline_main(config): return 0 +@attr.s(slots=True) +class KeywordMatcher: + """A matcher for keywords. + + Given a list of names, matches any substring of one of these names. The + string inclusion check is case-insensitive. + + Will match on the name of colitem, including the names of its parents. + Only matches names of items which are either a :class:`Class` or a + :class:`Function`. + + Additionally, matches on names in the 'extra_keyword_matches' set of + any item, as well as names directly assigned to test functions. + """ + + _names = attr.ib(type=AbstractSet[str]) + + @classmethod + def from_item(cls, item: "Item") -> "KeywordMatcher": + mapped_names = set() + + # Add the names of the current item and any parent items + import pytest + + for item in item.listchain(): + if not isinstance(item, pytest.Instance): + mapped_names.add(item.name) + + # Add the names added as extra keywords to current or parent items + mapped_names.update(item.listextrakeywords()) + + # Add the names attached to the current function through direct assignment + function_obj = getattr(item, "function", None) + if function_obj: + mapped_names.update(function_obj.__dict__) + + # add the markers to the keywords as we no longer handle them correctly + mapped_names.update(mark.name for mark in item.iter_markers()) + + return cls(mapped_names) + + def __call__(self, subname: str) -> bool: + subname = subname.lower() + names = (name.lower() for name in self._names) + + for name in names: + if subname in name: + return True + return False + + def deselect_by_keyword(items, config): keywordexpr = config.option.keyword.lstrip() if not keywordexpr: @@ -120,10 +178,17 @@ def deselect_by_keyword(items, config): selectuntil = True keywordexpr = keywordexpr[:-1] + try: + expression = Expression.compile(keywordexpr) + except ParseError as e: + raise UsageError( + "Wrong expression passed to '-k': {}: {}".format(keywordexpr, e) + ) from None + remaining = [] deselected = [] for colitem in items: - if keywordexpr and not matchkeyword(colitem, keywordexpr): + if keywordexpr and not expression.evaluate(KeywordMatcher.from_item(colitem)): deselected.append(colitem) else: if selectuntil: @@ -135,15 +200,40 @@ def deselect_by_keyword(items, config): items[:] = remaining +@attr.s(slots=True) +class MarkMatcher: + """A matcher for markers which are present. + + Tries to match on any marker names, attached to the given colitem. + """ + + own_mark_names = attr.ib() + + @classmethod + def from_item(cls, item) -> "MarkMatcher": + mark_names = {mark.name for mark in item.iter_markers()} + return cls(mark_names) + + def __call__(self, name: str) -> bool: + return name in self.own_mark_names + + def deselect_by_mark(items, config): matchexpr = config.option.markexpr if not matchexpr: return + try: + expression = Expression.compile(matchexpr) + except ParseError as e: + raise UsageError( + "Wrong expression passed to '-m': {}: {}".format(matchexpr, e) + ) from None + remaining = [] deselected = [] for item in items: - if matchmark(item, matchexpr): + if expression.evaluate(MarkMatcher.from_item(item)): remaining.append(item) else: deselected.append(item) diff --git a/src/_pytest/mark/legacy.py b/src/_pytest/mark/legacy.py deleted file mode 100644 index ed707fcc739..00000000000 --- a/src/_pytest/mark/legacy.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -this is a place where we put datastructures used by legacy apis -we hope to remove -""" -from typing import Set - -import attr - -from _pytest.compat import TYPE_CHECKING -from _pytest.config import UsageError -from _pytest.mark.expression import Expression -from _pytest.mark.expression import ParseError - -if TYPE_CHECKING: - from _pytest.nodes import Item - - -@attr.s -class MarkMatcher: - """A matcher for markers which are present.""" - - own_mark_names = attr.ib() - - @classmethod - def from_item(cls, item) -> "MarkMatcher": - mark_names = {mark.name for mark in item.iter_markers()} - return cls(mark_names) - - def __call__(self, name: str) -> bool: - return name in self.own_mark_names - - -@attr.s -class KeywordMatcher: - """A matcher for keywords. - - Given a list of names, matches any substring of one of these names. The - string inclusion check is case-insensitive. - """ - - _names = attr.ib(type=Set[str]) - - @classmethod - def from_item(cls, item: "Item") -> "KeywordMatcher": - mapped_names = set() - - # Add the names of the current item and any parent items - import pytest - - for item in item.listchain(): - if not isinstance(item, pytest.Instance): - mapped_names.add(item.name) - - # Add the names added as extra keywords to current or parent items - mapped_names.update(item.listextrakeywords()) - - # Add the names attached to the current function through direct assignment - function_obj = getattr(item, "function", None) - if function_obj: - mapped_names.update(function_obj.__dict__) - - # add the markers to the keywords as we no longer handle them correctly - mapped_names.update(mark.name for mark in item.iter_markers()) - - return cls(mapped_names) - - def __call__(self, subname: str) -> bool: - subname = subname.lower() - names = (name.lower() for name in self._names) - - for name in names: - if subname in name: - return True - return False - - -def matchmark(colitem, markexpr: str) -> bool: - """Tries to match on any marker names, attached to the given colitem.""" - try: - expression = Expression.compile(markexpr) - except ParseError as e: - raise UsageError( - "Wrong expression passed to '-m': {}: {}".format(markexpr, e) - ) from None - return expression.evaluate(MarkMatcher.from_item(colitem)) - - -def matchkeyword(colitem, keywordexpr: str) -> bool: - """Tries to match given keyword expression to given collector item. - - Will match on the name of colitem, including the names of its parents. - Only matches names of items which are either a :class:`Class` or a - :class:`Function`. - Additionally, matches on names in the 'extra_keyword_matches' set of - any item, as well as names directly assigned to test functions. - """ - try: - expression = Expression.compile(keywordexpr) - except ParseError as e: - raise UsageError( - "Wrong expression passed to '-k': {}: {}".format(keywordexpr, e) - ) from None - return expression.evaluate(KeywordMatcher.from_item(colitem)) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index fa0cfce9722..1d332145504 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -443,7 +443,7 @@ def test_one(): def test_unicode_args(testdir) -> None: - result = testdir.runpytest("-k", "💩") + result = testdir.runpytest("-k", "אבג") assert result.ret == ExitCode.NO_TESTS_COLLECTED From 4dfc461036d1a152caad802d09ebccbda2a8cd7f Mon Sep 17 00:00:00 2001 From: Felix Nieuwenhuizen Date: Wed, 27 Nov 2019 11:50:22 +0100 Subject: [PATCH 196/823] Create LogCaptureHandler if necessary (closes #6240) --- AUTHORS | 1 + changelog/6240.bugfix.rst | 2 ++ src/_pytest/logging.py | 8 +++++++- testing/test_capture.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 changelog/6240.bugfix.rst diff --git a/AUTHORS b/AUTHORS index b59ebc2a27f..01159f7d453 100644 --- a/AUTHORS +++ b/AUTHORS @@ -99,6 +99,7 @@ Erik M. Bray Evan Kepner Fabien Zarifian Fabio Zadrozny +Felix Nieuwenhuizen Feng Ma Florian Bruhin Floris Bruynooghe diff --git a/changelog/6240.bugfix.rst b/changelog/6240.bugfix.rst new file mode 100644 index 00000000000..b5f5844ec4a --- /dev/null +++ b/changelog/6240.bugfix.rst @@ -0,0 +1,2 @@ +Fixes an issue where logging during collection step caused duplication of log +messages to stderr. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 681fdee6213..8cb7b1841aa 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -613,7 +613,13 @@ def pytest_collection(self) -> Generator[None, None, None]: with catching_logs(self.log_file_handler, level=self.log_file_level): yield else: - yield + # Add a dummy handler to ensure logging.root.handlers is not empty. + # If it were empty, then a `logging.warning()` call (and similar) during collection + # would trigger a `logging.basicConfig()` call, which would add a `StreamHandler` + # handler, which would cause all subsequent logs which reach the root to be also + # printed to stdout, which we don't want (issue #6240). + with catching_logs(logging.NullHandler()): + yield @contextmanager def _runtest_for(self, item, when): diff --git a/testing/test_capture.py b/testing/test_capture.py index 23314319351..c064614d238 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1493,3 +1493,32 @@ def test__get_multicapture() -> None: pytest.raises(ValueError, _get_multicapture, "unknown").match( r"^unknown capturing method: 'unknown'" ) + + +def test_logging_while_collecting(testdir): + """Issue #6240: Calls to logging.xxx() during collection causes all logging calls to be duplicated to stderr""" + p = testdir.makepyfile( + """\ + import logging + + logging.warning("during collection") + + def test_logging(): + logging.warning("during call") + assert False + """ + ) + result = testdir.runpytest_subprocess(p) + assert result.ret == ExitCode.TESTS_FAILED + result.stdout.fnmatch_lines( + [ + "*test_*.py F*", + "====* FAILURES *====", + "____*____", + "*--- Captured log call*", + "WARNING * during call", + "*1 failed*", + ] + ) + result.stdout.no_fnmatch_line("*Captured stderr call*") + result.stdout.no_fnmatch_line("*during collection*") From d530d701287f85d66ba9c9109b758d50d0f20194 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 15 Mar 2020 13:26:04 +0100 Subject: [PATCH 197/823] Fix regressions with `--lf` plugin Only filter with known failures, and explicitly keep paths of passed arguments. This also displays the "run-last-failure" status before collected files, and does not update the cache with "--collect-only". Fixes https://github.com/pytest-dev/pytest/issues/6968. --- changelog/6991.bugfix.rst | 1 + changelog/6991.improvement.rst | 1 + src/_pytest/cacheprovider.py | 58 ++++++---- src/_pytest/terminal.py | 8 +- testing/test_cacheprovider.py | 206 ++++++++++++++++++++++++++------- 5 files changed, 210 insertions(+), 64 deletions(-) create mode 100644 changelog/6991.bugfix.rst create mode 100644 changelog/6991.improvement.rst diff --git a/changelog/6991.bugfix.rst b/changelog/6991.bugfix.rst new file mode 100644 index 00000000000..879354e2533 --- /dev/null +++ b/changelog/6991.bugfix.rst @@ -0,0 +1 @@ +Fix regressions with `--lf` filtering too much since pytest 5.4. diff --git a/changelog/6991.improvement.rst b/changelog/6991.improvement.rst new file mode 100644 index 00000000000..ec08b66c27e --- /dev/null +++ b/changelog/6991.improvement.rst @@ -0,0 +1 @@ +Collected files are displayed after any reports from hooks, e.g. the status from ``--lf``. diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index a48bd9d6fbf..ce820ca2b90 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -183,27 +183,35 @@ def pytest_make_collect_report(self, collector) -> Generator: res.result = sorted( res.result, key=lambda x: 0 if Path(str(x.fspath)) in lf_paths else 1, ) - out.force_result(res) return elif isinstance(collector, Module): if Path(str(collector.fspath)) in self.lfplugin._last_failed_paths: out = yield res = out.get_result() - - filtered_result = [ - x for x in res.result if x.nodeid in self.lfplugin.lastfailed + 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 + self.lfplugin.config.pluginmanager.register( + LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip" + ) + self._collected_at_least_one_failure = True + + session = collector.session + result[:] = [ + x + for x in result + if x.nodeid in lastfailed + # Include any passed arguments (not trivial to filter). + or session.isinitpath(x.fspath) + # Keep all sub-collectors. + or isinstance(x, nodes.Collector) ] - if filtered_result: - res.result = filtered_result - out.force_result(res) - - if not self._collected_at_least_one_failure: - self.lfplugin.config.pluginmanager.register( - LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip" - ) - self._collected_at_least_one_failure = True - return res + return yield @@ -234,8 +242,8 @@ def __init__(self, config: Config) -> None: self.lastfailed = config.cache.get( "cache/lastfailed", {} ) # type: Dict[str, bool] - self._previously_failed_count = None - self._report_status = None + self._previously_failed_count = None # type: Optional[int] + self._report_status = None # type: Optional[str] self._skipped_files = 0 # count skipped files during collection due to --lf if config.getoption("lf"): @@ -269,7 +277,12 @@ def pytest_collectreport(self, report): else: self.lastfailed[report.nodeid] = True - def pytest_collection_modifyitems(self, session, config, items): + @pytest.hookimpl(hookwrapper=True, tryfirst=True) + def pytest_collection_modifyitems( + self, config: Config, items: List[nodes.Item] + ) -> Generator[None, None, None]: + yield + if not self.active: return @@ -334,9 +347,12 @@ def __init__(self, config): self.active = config.option.newfirst self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) + @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection_modifyitems( - self, session: Session, config: Config, items: List[nodes.Item] - ) -> None: + self, items: List[nodes.Item] + ) -> Generator[None, None, None]: + yield + if self.active: new_items = order_preserving_dict() # type: Dict[str, nodes.Item] other_items = order_preserving_dict() # type: Dict[str, nodes.Item] @@ -356,11 +372,13 @@ def pytest_collection_modifyitems( def _get_increasing_order(self, items): return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) - def pytest_sessionfinish(self, session): + def pytest_sessionfinish(self) -> None: config = self.config if config.getoption("cacheshow") or hasattr(config, "slaveinput"): return + if config.getoption("collectonly"): + return config.cache.set("cache/nodeids", sorted(self.cached_nodeids)) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 328f3418d90..3de0612bf4b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -664,15 +664,17 @@ def pytest_report_header(self, config): def pytest_collection_finish(self, session): self.report_collect(True) - if self.config.getoption("collectonly"): - self._printcollecteditems(session.items) - lines = self.config.hook.pytest_report_collectionfinish( config=self.config, startdir=self.startdir, items=session.items ) self._write_report_lines_from_hooks(lines) if self.config.getoption("collectonly"): + if session.items: + if self.config.option.verbose > -1: + self._tw.line("") + self._printcollecteditems(session.items) + failed = self.stats.get("failed") if failed: self._tw.sep("!", "collection failures") diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 6ab5d9a1d01..c133663ea1b 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -7,6 +7,7 @@ import pytest from _pytest.config import ExitCode +from _pytest.pytester import Testdir pytest_plugins = ("pytester",) @@ -267,9 +268,9 @@ def test_3(): assert 0 result = testdir.runpytest(str(p), "--lf") result.stdout.fnmatch_lines( [ - "collected 2 items", + "collected 3 items / 1 deselected / 2 selected", "run-last-failure: rerun previous 2 failures", - "*= 2 passed in *", + "*= 2 passed, 1 deselected in *", ] ) result = testdir.runpytest(str(p), "--lf") @@ -345,7 +346,13 @@ def test_a2(): assert 1 result = testdir.runpytest("--lf", p2) result.stdout.fnmatch_lines(["*1 passed*"]) result = testdir.runpytest("--lf", p) - result.stdout.fnmatch_lines(["collected 1 item", "*= 1 failed in *"]) + result.stdout.fnmatch_lines( + [ + "collected 2 items / 1 deselected / 1 selected", + "run-last-failure: rerun previous 1 failure", + "*= 1 failed, 1 deselected in *", + ] + ) def test_lastfailed_usecase_splice(self, testdir, monkeypatch): monkeypatch.setattr("sys.dont_write_bytecode", True) @@ -690,9 +697,9 @@ def test_foo_4(): pass result = testdir.runpytest(test_foo, "--last-failed") result.stdout.fnmatch_lines( [ - "collected 1 item", + "collected 2 items / 1 deselected / 1 selected", "run-last-failure: rerun previous 1 failure", - "*= 1 passed in *", + "*= 1 passed, 1 deselected in *", ] ) assert self.get_cached_last_failed(testdir) == [] @@ -838,7 +845,7 @@ def test_lastfailed_with_known_failures_not_being_selected(self, testdir): ] ) - # Remove/rename test. + # Remove/rename test: collects the file again. testdir.makepyfile(**{"pkg1/test_1.py": """def test_renamed(): assert 0"""}) result = testdir.runpytest("--lf", "-rf") result.stdout.fnmatch_lines( @@ -852,6 +859,133 @@ def test_lastfailed_with_known_failures_not_being_selected(self, testdir): ] ) + result = testdir.runpytest("--lf", "--co") + result.stdout.fnmatch_lines( + [ + "collected 1 item", + "run-last-failure: rerun previous 1 failure (skipped 1 file)", + "", + "", + " ", + ] + ) + + def test_lastfailed_args_with_deselected(self, testdir: Testdir) -> None: + """Test regression with --lf running into NoMatch error. + + This was caused by it not collecting (non-failed) nodes given as + arguments. + """ + testdir.makepyfile( + **{ + "pkg1/test_1.py": """ + def test_pass(): pass + def test_fail(): assert 0 + """, + } + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"]) + assert result.ret == 1 + + result = testdir.runpytest("pkg1/test_1.py::test_pass", "--lf", "--co") + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + "*collected 1 item", + "run-last-failure: 1 known failures not in selected tests", + "", + "", + " ", + ], + consecutive=True, + ) + + result = testdir.runpytest( + "pkg1/test_1.py::test_pass", "pkg1/test_1.py::test_fail", "--lf", "--co" + ) + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + "collected 2 items / 1 deselected / 1 selected", + "run-last-failure: rerun previous 1 failure", + "", + "", + " ", + "*= 1 deselected in *", + ], + ) + + def test_lastfailed_with_class_items(self, testdir: Testdir) -> None: + """Test regression with --lf deselecting whole classes.""" + testdir.makepyfile( + **{ + "pkg1/test_1.py": """ + class TestFoo: + def test_pass(self): pass + def test_fail(self): assert 0 + + def test_other(): assert 0 + """, + } + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["collected 3 items", "* 2 failed, 1 passed in *"]) + assert result.ret == 1 + + result = testdir.runpytest("--lf", "--co") + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + "collected 3 items / 1 deselected / 2 selected", + "run-last-failure: rerun previous 2 failures", + "", + "", + " ", + " ", + " ", + "", + "*= 1 deselected in *", + ], + consecutive=True, + ) + + def test_lastfailed_with_all_filtered(self, testdir: Testdir) -> None: + testdir.makepyfile( + **{ + "pkg1/test_1.py": """ + def test_fail(): assert 0 + def test_pass(): pass + """, + } + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"]) + assert result.ret == 1 + + # Remove known failure. + testdir.makepyfile( + **{ + "pkg1/test_1.py": """ + def test_pass(): pass + """, + } + ) + result = testdir.runpytest("--lf", "--co") + result.stdout.fnmatch_lines( + [ + "collected 1 item", + "run-last-failure: 1 known failures not in selected tests", + "", + "", + " ", + "", + "*= no tests ran in*", + ], + consecutive=True, + ) + assert result.ret == 0 + class TestNewFirst: def test_newfirst_usecase(self, testdir): @@ -859,63 +993,54 @@ def test_newfirst_usecase(self, testdir): **{ "test_1/test_1.py": """ def test_1(): assert 1 - def test_2(): assert 1 - def test_3(): assert 1 """, "test_2/test_2.py": """ def test_1(): assert 1 - def test_2(): assert 1 - def test_3(): assert 1 """, } ) - testdir.tmpdir.join("test_1/test_1.py").setmtime(1) result = testdir.runpytest("-v") result.stdout.fnmatch_lines( - [ - "*test_1/test_1.py::test_1 PASSED*", - "*test_1/test_1.py::test_2 PASSED*", - "*test_1/test_1.py::test_3 PASSED*", - "*test_2/test_2.py::test_1 PASSED*", - "*test_2/test_2.py::test_2 PASSED*", - "*test_2/test_2.py::test_3 PASSED*", - ] + ["*test_1/test_1.py::test_1 PASSED*", "*test_2/test_2.py::test_1 PASSED*"] ) result = testdir.runpytest("-v", "--nf") - result.stdout.fnmatch_lines( - [ - "*test_2/test_2.py::test_1 PASSED*", - "*test_2/test_2.py::test_2 PASSED*", - "*test_2/test_2.py::test_3 PASSED*", - "*test_1/test_1.py::test_1 PASSED*", - "*test_1/test_1.py::test_2 PASSED*", - "*test_1/test_1.py::test_3 PASSED*", - ] + ["*test_2/test_2.py::test_1 PASSED*", "*test_1/test_1.py::test_1 PASSED*"] ) testdir.tmpdir.join("test_1/test_1.py").write( - "def test_1(): assert 1\n" - "def test_2(): assert 1\n" - "def test_3(): assert 1\n" - "def test_4(): assert 1\n" + "def test_1(): assert 1\n" "def test_2(): assert 1\n" ) testdir.tmpdir.join("test_1/test_1.py").setmtime(1) - result = testdir.runpytest("-v", "--nf") + result = testdir.runpytest("--nf", "--collect-only", "-q") + result.stdout.fnmatch_lines( + [ + "test_1/test_1.py::test_2", + "test_2/test_2.py::test_1", + "test_1/test_1.py::test_1", + ] + ) + # Newest first with (plugin) pytest_collection_modifyitems hook. + testdir.makepyfile( + myplugin=""" + def pytest_collection_modifyitems(items): + items[:] = sorted(items, key=lambda item: item.nodeid) + print("new_items:", [x.nodeid for x in items]) + """ + ) + testdir.syspathinsert() + result = testdir.runpytest("--nf", "-p", "myplugin", "--collect-only", "-q") result.stdout.fnmatch_lines( [ - "*test_1/test_1.py::test_4 PASSED*", - "*test_2/test_2.py::test_1 PASSED*", - "*test_2/test_2.py::test_2 PASSED*", - "*test_2/test_2.py::test_3 PASSED*", - "*test_1/test_1.py::test_1 PASSED*", - "*test_1/test_1.py::test_2 PASSED*", - "*test_1/test_1.py::test_3 PASSED*", + "new_items: *test_1.py*test_1.py*test_2.py*", + "test_1/test_1.py::test_2", + "test_2/test_2.py::test_1", + "test_1/test_1.py::test_1", ] ) @@ -948,7 +1073,6 @@ def test_1(num): assert num ) result = testdir.runpytest("-v", "--nf") - result.stdout.fnmatch_lines( [ "*test_2/test_2.py::test_1[1*", From e1becae24ca1e6695026d0ed407a2cc7402b4829 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 16 May 2020 13:42:39 -0300 Subject: [PATCH 198/823] Fix errors introduced by #6911 Somehow I've missed the failures while merging, totally my fault. --- scripts/release-on-comment.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/release-on-comment.py b/scripts/release-on-comment.py index 5ed71f23675..521a19faca6 100644 --- a/scripts/release-on-comment.py +++ b/scripts/release-on-comment.py @@ -31,11 +31,10 @@ import re import sys from pathlib import Path +from subprocess import CalledProcessError from subprocess import check_call from subprocess import check_output -from subprocess import PIPE from subprocess import run -from subprocess import STDOUT from textwrap import dedent from typing import Dict from typing import Optional @@ -178,7 +177,7 @@ def trigger_release(payload_path: Path, token: str) -> None: print(f"Notified in original comment {Fore.CYAN}{comment.url}{Fore.RESET}.") print(f"{Fore.GREEN}Success.") - except CallProcessError as e: + except CalledProcessError as e: error_contents = e.output except Exception as e: error_contents = str(e) @@ -213,7 +212,7 @@ def trigger_release(payload_path: Path, token: str) -> None: """ ) ) - print_and_exit(f"{Fore.RED}{e}") + print_and_exit(f"{Fore.RED}{error_contents}") def find_next_version(base_branch: str) -> str: From 3d3b9511fd1a6a18d15fd71f18ab62356c9005c0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 8 Apr 2020 18:52:48 +0200 Subject: [PATCH 199/823] -k should not match session name Fixes https://github.com/pytest-dev/pytest/issues/7040. --- changelog/7040.improvement.rst | 1 + src/_pytest/mark/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/7040.improvement.rst diff --git a/changelog/7040.improvement.rst b/changelog/7040.improvement.rst new file mode 100644 index 00000000000..267eac4c46e --- /dev/null +++ b/changelog/7040.improvement.rst @@ -0,0 +1 @@ +``-k`` no longer matches against the directory containing the test suite. diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 134ed187641..fd003f68b25 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -136,7 +136,7 @@ def from_item(cls, item: "Item") -> "KeywordMatcher": import pytest for item in item.listchain(): - if not isinstance(item, pytest.Instance): + if not isinstance(item, (pytest.Instance, pytest.Session)): mapped_names.add(item.name) # Add the names added as extra keywords to current or parent items From c5b367b4f4140c23ec9ebcca8ff5bad5937407a3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 16 May 2020 15:01:26 -0300 Subject: [PATCH 200/823] Package.name now contains only basname of the package Previously it contained the entire path, which made '-k' match against any name in the full path of the package. Fix #7040 --- changelog/7040.breaking.rst | 6 ++++++ changelog/7040.improvement.rst | 1 - src/_pytest/python.py | 3 +-- testing/test_collection.py | 12 ++++++------ testing/test_mark.py | 30 ++++++++++++++++++++++++++++++ 5 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 changelog/7040.breaking.rst delete mode 100644 changelog/7040.improvement.rst diff --git a/changelog/7040.breaking.rst b/changelog/7040.breaking.rst new file mode 100644 index 00000000000..897b2c0a972 --- /dev/null +++ b/changelog/7040.breaking.rst @@ -0,0 +1,6 @@ +``-k`` no longer matches against the names of the directories outside the test session root. + +Also, ``pytest.Package.name`` is now just the name of the directory containing the package's +``__init__.py`` file, instead of the full path. This is consistent with how the other nodes +are named, and also one of the reasons why ``-k`` would match against any directory containing +the test suite. diff --git a/changelog/7040.improvement.rst b/changelog/7040.improvement.rst deleted file mode 100644 index 267eac4c46e..00000000000 --- a/changelog/7040.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``-k`` no longer matches against the directory containing the test suite. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index d3acfad4458..e2a6d82c618 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -568,8 +568,7 @@ def __init__( nodes.FSCollector.__init__( self, fspath, parent=parent, config=config, session=session, nodeid=nodeid ) - - self.name = fspath.dirname + self.name = os.path.basename(str(fspath.dirname)) def setup(self): # not using fixtures to call setup_module here because autouse fixtures diff --git a/testing/test_collection.py b/testing/test_collection.py index 2fd832dc6b7..dfbfe9ba818 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1004,7 +1004,7 @@ def test_collect_init_tests(testdir): result.stdout.fnmatch_lines( [ "collected 2 items", - "", " ", " ", " ", @@ -1015,7 +1015,7 @@ def test_collect_init_tests(testdir): result.stdout.fnmatch_lines( [ "collected 2 items", - "", " ", " ", " ", @@ -1027,7 +1027,7 @@ def test_collect_init_tests(testdir): result.stdout.fnmatch_lines( [ "collected 2 items", - "", + "", " ", " ", " ", @@ -1039,7 +1039,7 @@ def test_collect_init_tests(testdir): result.stdout.fnmatch_lines( [ "collected 2 items", - "", + "", " ", " ", " ", @@ -1048,12 +1048,12 @@ def test_collect_init_tests(testdir): ) result = testdir.runpytest("./tests/test_foo.py", "--collect-only") result.stdout.fnmatch_lines( - ["", " ", " "] + ["", " ", " "] ) result.stdout.no_fnmatch_line("*test_init*") result = testdir.runpytest("./tests/__init__.py", "--collect-only") result.stdout.fnmatch_lines( - ["", " ", " "] + ["", " ", " "] ) result.stdout.no_fnmatch_line("*test_foo*") diff --git a/testing/test_mark.py b/testing/test_mark.py index 1c983b5af21..c14f770daa4 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -834,6 +834,36 @@ def test_one(): assert 1 deselected_tests = dlist[0].items assert len(deselected_tests) == 1 + def test_no_match_directories_outside_the_suite(self, testdir): + """ + -k should not match against directories containing the test suite (#7040). + """ + test_contents = """ + def test_aaa(): pass + def test_ddd(): pass + """ + testdir.makepyfile( + **{"ddd/tests/__init__.py": "", "ddd/tests/test_foo.py": test_contents} + ) + + def get_collected_names(*args): + _, rec = testdir.inline_genitems(*args) + calls = rec.getcalls("pytest_collection_finish") + assert len(calls) == 1 + return [x.name for x in calls[0].session.items] + + # sanity check: collect both tests in normal runs + assert get_collected_names() == ["test_aaa", "test_ddd"] + + # do not collect anything based on names outside the collection tree + assert get_collected_names("-k", testdir.tmpdir.basename) == [] + + # "-k ddd" should only collect "test_ddd", but not + # 'test_aaa' just because one of its parent directories is named "ddd"; + # this was matched previously because Package.name would contain the full path + # to the package + assert get_collected_names("-k", "ddd") == ["test_ddd"] + class TestMarkDecorator: @pytest.mark.parametrize( From 5eaebc1900e2c5c0fb911ff9c325efa80441d030 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 16 May 2020 15:46:06 -0300 Subject: [PATCH 201/823] Remove one of the tracebacks from conftest import failures This removes the KeyError from the traceback chain when an conftest fails to import: return self._conftestpath2mod[key] E KeyError: WindowsPath('D:/projects/pytest/.tmp/root/foo/conftest.py') During handling of the above exception, another exception occurred: ... raise RuntimeError("some error") E RuntimeError: some error During handling of the above exception, another exception occurred: ... E _pytest.config.ConftestImportFailure: (...) By slightly changing the code, we can remove the first chain, which is often very confusing to users and doesn't help with anything. Fix #7223 --- src/_pytest/config/__init__.py | 57 ++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 8a2c16d5d5e..d45feb624ed 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1,5 +1,6 @@ """ command line options, ini-file and conftest.py processing. """ import argparse +import contextlib import copy import enum import inspect @@ -511,34 +512,36 @@ def _importconftest(self, conftestpath): # Using Path().resolve() is better than py.path.realpath because # it resolves to the correct path/drive in case-insensitive file systems (#5792) key = Path(str(conftestpath)).resolve() - try: + + with contextlib.suppress(KeyError): return self._conftestpath2mod[key] - except KeyError: - pkgpath = conftestpath.pypkgpath() - if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.purebasename) - try: - mod = conftestpath.pyimport() - if ( - hasattr(mod, "pytest_plugins") - and self._configured - and not self._using_pyargs - ): - _fail_on_non_top_pytest_plugins(conftestpath, self._confcutdir) - except Exception: - raise ConftestImportFailure(conftestpath, sys.exc_info()) - - self._conftest_plugins.add(mod) - self._conftestpath2mod[key] = mod - dirpath = conftestpath.dirpath() - if dirpath in self._dirpath2confmods: - for path, mods in self._dirpath2confmods.items(): - if path and path.relto(dirpath) or path == dirpath: - assert mod not in mods - mods.append(mod) - self.trace("loading conftestmodule {!r}".format(mod)) - self.consider_conftest(mod) - return mod + + pkgpath = conftestpath.pypkgpath() + if pkgpath is None: + _ensure_removed_sysmodule(conftestpath.purebasename) + + try: + mod = conftestpath.pyimport() + if ( + hasattr(mod, "pytest_plugins") + and self._configured + and not self._using_pyargs + ): + _fail_on_non_top_pytest_plugins(conftestpath, self._confcutdir) + except Exception as e: + raise ConftestImportFailure(conftestpath, sys.exc_info()) from e + + self._conftest_plugins.add(mod) + self._conftestpath2mod[key] = mod + dirpath = conftestpath.dirpath() + if dirpath in self._dirpath2confmods: + for path, mods in self._dirpath2confmods.items(): + if path and path.relto(dirpath) or path == dirpath: + assert mod not in mods + mods.append(mod) + self.trace("loading conftestmodule {!r}".format(mod)) + self.consider_conftest(mod) + return mod # # API for bootstrapping plugin loading From 9e1e7fcabe4ebcdbcdadbd1f3eb8866f3c1821a7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 16 May 2020 16:05:12 -0300 Subject: [PATCH 202/823] Use a nice string repr for ConftestImportFailure The default message is often hard to read: E _pytest.config.ConftestImportFailure: (local('D:\\projects\\pytest\\.tmp\\root\\foo\\conftest.py'), (, RuntimeError('some error',), )) Using a shorter message is better: E _pytest.config.ConftestImportFailure: RuntimeError: some error (from D:\projects\pytest\.tmp\root\foo\conftest.py) And we don't really lose any information due to exception chaining. --- src/_pytest/config/__init__.py | 7 ++++++- testing/test_config.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index d45feb624ed..a4fc6e7c131 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -88,10 +88,15 @@ class ExitCode(enum.IntEnum): class ConftestImportFailure(Exception): def __init__(self, path, excinfo): - Exception.__init__(self, path, excinfo) + super().__init__(path, excinfo) self.path = path self.excinfo = excinfo # type: Tuple[Type[Exception], Exception, TracebackType] + def __str__(self): + return "{}: {} (from {})".format( + self.excinfo[0].__name__, self.excinfo[1], self.path + ) + def main(args=None, plugins=None) -> Union[int, ExitCode]: """ return exit code, after performing an in-process test run. diff --git a/testing/test_config.py b/testing/test_config.py index f6bf0499fe0..7d553e63b7c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -10,6 +10,7 @@ from _pytest.compat import importlib_metadata from _pytest.config import _iter_rewritable_modules from _pytest.config import Config +from _pytest.config import ConftestImportFailure from _pytest.config import ExitCode from _pytest.config.exceptions import UsageError from _pytest.config.findpaths import determine_setup @@ -1471,3 +1472,19 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives assert res.ret == 0 msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" assert msg not in res.stdout.str() + + +def test_conftest_import_error_repr(tmpdir): + """ + ConftestImportFailure should use a short error message and readable path to the failed + conftest.py file + """ + path = tmpdir.join("foo/conftest.py") + with pytest.raises( + ConftestImportFailure, + match=re.escape("RuntimeError: some error (from {})".format(path)), + ): + try: + raise RuntimeError("some error") + except Exception: + raise ConftestImportFailure(path, sys.exc_info()) From c26f389c09479bc34a0ddb8a531089fa58f9bd80 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 17 May 2020 11:20:56 -0300 Subject: [PATCH 203/823] Refactor handling of non-top-level pytest_plugins handling Decided to move the 'if' logic together with the error message, as this leaves the _importconftest method cleaner. --- src/_pytest/config/__init__.py | 38 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index a4fc6e7c131..68c3822d05e 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -286,19 +286,6 @@ def _prepareconfig( raise -def _fail_on_non_top_pytest_plugins(conftestpath, confcutdir): - msg = ( - "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n" - "It affects the entire test suite instead of just below the conftest as expected.\n" - " {}\n" - "Please move it to a top level conftest file at the rootdir:\n" - " {}\n" - "For more information, visit:\n" - " https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" - ) - fail(msg.format(conftestpath, confcutdir), pytrace=False) - - class PytestPluginManager(PluginManager): """ Overwrites :py:class:`pluggy.PluginManager ` to add pytest-specific @@ -527,15 +514,11 @@ def _importconftest(self, conftestpath): try: mod = conftestpath.pyimport() - if ( - hasattr(mod, "pytest_plugins") - and self._configured - and not self._using_pyargs - ): - _fail_on_non_top_pytest_plugins(conftestpath, self._confcutdir) except Exception as e: raise ConftestImportFailure(conftestpath, sys.exc_info()) from e + self._check_non_top_pytest_plugins(mod, conftestpath) + self._conftest_plugins.add(mod) self._conftestpath2mod[key] = mod dirpath = conftestpath.dirpath() @@ -548,6 +531,23 @@ def _importconftest(self, conftestpath): self.consider_conftest(mod) return mod + def _check_non_top_pytest_plugins(self, mod, conftestpath): + if ( + hasattr(mod, "pytest_plugins") + and self._configured + and not self._using_pyargs + ): + msg = ( + "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n" + "It affects the entire test suite instead of just below the conftest as expected.\n" + " {}\n" + "Please move it to a top level conftest file at the rootdir:\n" + " {}\n" + "For more information, visit:\n" + " https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" + ) + fail(msg.format(conftestpath, self._confcutdir), pytrace=False) + # # API for bootstrapping plugin loading # From ac6c02f1e2229d78609dede5ad96a0a72bdefdc7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 204/823] logging: use item's store for private attributes This makes things type-safe and properly private. --- changelog/7224.breaking.rst | 2 ++ src/_pytest/logging.py | 23 +++++++++++++---------- testing/logging/test_fixture.py | 3 ++- 3 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 changelog/7224.breaking.rst diff --git a/changelog/7224.breaking.rst b/changelog/7224.breaking.rst new file mode 100644 index 00000000000..32ab2c0734d --- /dev/null +++ b/changelog/7224.breaking.rst @@ -0,0 +1,2 @@ +The `item.catch_log_handler` and `item.catch_log_handlers` attributes, set by the +logging plugin and never meant to be public , are no longer available. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 8cb7b1841aa..4de5c1b2b21 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -17,10 +17,14 @@ from _pytest.config import Config from _pytest.config import create_terminal_writer from _pytest.pathlib import Path +from _pytest.store import StoreKey + DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" _ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m") +catch_log_handler_key = StoreKey["LogCaptureHandler"]() +catch_log_handlers_key = StoreKey[Dict[str, "LogCaptureHandler"]]() def _remove_ansi_escape_sequences(text): @@ -317,7 +321,7 @@ def reset(self) -> None: class LogCaptureFixture: """Provides access and control of log capturing.""" - def __init__(self, item) -> None: + def __init__(self, item: nodes.Node) -> None: """Creates a new funcarg.""" self._item = item # dict of log name -> log level @@ -338,7 +342,7 @@ def handler(self) -> LogCaptureHandler: """ :rtype: LogCaptureHandler """ - return self._item.catch_log_handler # type: ignore[no-any-return] + return self._item._store[catch_log_handler_key] def get_records(self, when: str) -> List[logging.LogRecord]: """ @@ -352,9 +356,9 @@ def get_records(self, when: str) -> List[logging.LogRecord]: .. versionadded:: 3.4 """ - handler = self._item.catch_log_handlers.get(when) + handler = self._item._store[catch_log_handlers_key].get(when) if handler: - return handler.records # type: ignore[no-any-return] + return handler.records else: return [] @@ -645,16 +649,15 @@ def _runtest_for_main( yield # run the test return - if not hasattr(item, "catch_log_handlers"): - item.catch_log_handlers = {} # type: ignore[attr-defined] - item.catch_log_handlers[when] = log_handler # type: ignore[attr-defined] - item.catch_log_handler = log_handler # type: ignore[attr-defined] + empty = {} # type: Dict[str, LogCaptureHandler] + item._store.setdefault(catch_log_handlers_key, empty)[when] = log_handler + item._store[catch_log_handler_key] = log_handler try: yield # run test finally: if when == "teardown": - del item.catch_log_handler # type: ignore[attr-defined] - del item.catch_log_handlers # type: ignore[attr-defined] + del item._store[catch_log_handlers_key] + del item._store[catch_log_handler_key] if self.print_logs: # Add a captured log section to the report. diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index c68866beff9..a33b0e80ecd 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -1,6 +1,7 @@ import logging import pytest +from _pytest.logging import catch_log_handlers_key logger = logging.getLogger(__name__) sublogger = logging.getLogger(__name__ + ".baz") @@ -136,4 +137,4 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"] # This reaches into private API, don't use this type of thing in real tests! - assert set(caplog._item.catch_log_handlers.keys()) == {"setup", "call"} + assert set(caplog._item._store[catch_log_handlers_key]) == {"setup", "call"} From 9effbe7425b2477e1593f8b6ffc6524e587a3be6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 205/823] logging: inline _runtest_for_main into _runtest_for This avoids a little bit of overhead, and makes the code a bit clearer too. --- src/_pytest/logging.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 4de5c1b2b21..61d68926390 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -626,17 +626,8 @@ def pytest_collection(self) -> Generator[None, None, None]: yield @contextmanager - def _runtest_for(self, item, when): - with self._runtest_for_main(item, when): - if self.log_file_handler is not None: - with catching_logs(self.log_file_handler, level=self.log_file_level): - yield - else: - yield - - @contextmanager - def _runtest_for_main( - self, item: nodes.Item, when: str + def _runtest_for( + self, item: Optional[nodes.Item], when: str ) -> Generator[None, None, None]: """Implements the internals of pytest_runtest_xxx() hook.""" with catching_logs( @@ -645,21 +636,26 @@ def _runtest_for_main( if self.log_cli_handler: self.log_cli_handler.set_when(when) - if item is None: - yield # run the test - return - - empty = {} # type: Dict[str, LogCaptureHandler] - item._store.setdefault(catch_log_handlers_key, empty)[when] = log_handler - item._store[catch_log_handler_key] = log_handler + if item is not None: + empty = {} # type: Dict[str, LogCaptureHandler] + item._store.setdefault(catch_log_handlers_key, empty)[ + when + ] = log_handler + item._store[catch_log_handler_key] = log_handler try: - yield # run test + if self.log_file_handler is not None: + with catching_logs( + self.log_file_handler, level=self.log_file_level + ): + yield + else: + yield finally: - if when == "teardown": + if item is not None and when == "teardown": del item._store[catch_log_handlers_key] del item._store[catch_log_handler_key] - if self.print_logs: + if item is not None and self.print_logs: # Add a captured log section to the report. log = log_handler.stream.getvalue().strip() item.add_report_section(when, "log", log) From ce0f2187936cebed1a75202530a672249c3f5438 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 206/823] logging: yield from _runtest_for instead of contextmanager Avoid the slight overhead of contextmanager. --- src/_pytest/logging.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 61d68926390..55175cec506 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -625,7 +625,6 @@ def pytest_collection(self) -> Generator[None, None, None]: with catching_logs(logging.NullHandler()): yield - @contextmanager def _runtest_for( self, item: Optional[nodes.Item], when: str ) -> Generator[None, None, None]: @@ -662,35 +661,29 @@ def _runtest_for( @pytest.hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item): - with self._runtest_for(item, "setup"): - yield + yield from self._runtest_for(item, "setup") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): - with self._runtest_for(item, "call"): - yield + yield from self._runtest_for(item, "call") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item): - with self._runtest_for(item, "teardown"): - yield + yield from self._runtest_for(item, "teardown") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_logstart(self): if self.log_cli_handler: self.log_cli_handler.reset() - with self._runtest_for(None, "start"): - yield + yield from self._runtest_for(None, "start") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_logfinish(self): - with self._runtest_for(None, "finish"): - yield + yield from self._runtest_for(None, "finish") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_logreport(self): - with self._runtest_for(None, "logreport"): - yield + yield from self._runtest_for(None, "logreport") @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionfinish(self): From eceb28e4bedde9ba4dfa8541e7fd92729f33c370 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 207/823] logging: set formatter on handler creation, not in catching_logs Conceptually it doesn't check per catching_logs (and catching_logs doesn't restore the older one either). It is just something that is defined for each handler once. --- src/_pytest/logging.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 55175cec506..9360b85c9c1 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -273,12 +273,10 @@ def add_option_ini(option, dest, default=None, type=None, **kwargs): @contextmanager -def catching_logs(handler, formatter=None, level=None): +def catching_logs(handler, level=None): """Context manager that prepares the whole logging machinery properly.""" root_logger = logging.getLogger() - if formatter is not None: - handler.setFormatter(formatter) if level is not None: handler.setLevel(level) @@ -303,15 +301,17 @@ def catching_logs(handler, formatter=None, level=None): class LogCaptureHandler(logging.StreamHandler): """A logging handler that stores log records and the log text.""" + stream = None # type: StringIO + def __init__(self) -> None: """Creates a new log handler.""" - logging.StreamHandler.__init__(self, StringIO()) + super().__init__(StringIO()) self.records = [] # type: List[logging.LogRecord] def emit(self, record: logging.LogRecord) -> None: """Keep the log records in a list in addition to the log text.""" self.records.append(record) - logging.StreamHandler.emit(self, record) + super().emit(record) def reset(self) -> None: self.records = [] @@ -571,11 +571,12 @@ def _setup_cli_logging(self): get_option_ini(config, "log_cli_date_format", "log_date_format"), get_option_ini(config, "log_auto_indent"), ) + log_cli_handler.setFormatter(log_cli_formatter) log_cli_level = get_log_level_for_setting(config, "log_cli_level", "log_level") self.log_cli_handler = log_cli_handler self.live_logs_context = lambda: catching_logs( - log_cli_handler, formatter=log_cli_formatter, level=log_cli_level + log_cli_handler, level=log_cli_level ) def set_log_path(self, fname): @@ -629,9 +630,9 @@ def _runtest_for( self, item: Optional[nodes.Item], when: str ) -> Generator[None, None, None]: """Implements the internals of pytest_runtest_xxx() hook.""" - with catching_logs( - LogCaptureHandler(), formatter=self.formatter, level=self.log_level - ) as log_handler: + log_handler = LogCaptureHandler() + log_handler.setFormatter(self.formatter) + with catching_logs(log_handler, level=self.log_level): if self.log_cli_handler: self.log_cli_handler.set_when(when) From e48ac692dedc11e5c5c8722b426f5b575eeadea0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 208/823] logging: optimize catching_logs slightly Remove usage of `@contextmanager` as it is a bit slower than hand-rolling, and also disallows re-entry which we want to use. Removing protections around addHandler()/removeHandler(), because logging already checks that internally. --- src/_pytest/logging.py | 51 ++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 9360b85c9c1..fe20153721c 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -272,30 +272,31 @@ def add_option_ini(option, dest, default=None, type=None, **kwargs): ) -@contextmanager -def catching_logs(handler, level=None): +# Not using @contextmanager for performance reasons. +class catching_logs: """Context manager that prepares the whole logging machinery properly.""" - root_logger = logging.getLogger() - if level is not None: - handler.setLevel(level) + __slots__ = ("handler", "level", "orig_level") - # Adding the same handler twice would confuse logging system. - # Just don't do that. - add_new_handler = handler not in root_logger.handlers + def __init__(self, handler, level=None): + self.handler = handler + self.level = level - if add_new_handler: - root_logger.addHandler(handler) - if level is not None: - orig_level = root_logger.level - root_logger.setLevel(min(orig_level, level)) - try: - yield handler - finally: - if level is not None: - root_logger.setLevel(orig_level) - if add_new_handler: - root_logger.removeHandler(handler) + def __enter__(self): + root_logger = logging.getLogger() + if self.level is not None: + self.handler.setLevel(self.level) + root_logger.addHandler(self.handler) + if self.level is not None: + self.orig_level = root_logger.level + root_logger.setLevel(min(self.orig_level, self.level)) + return self.handler + + def __exit__(self, type, value, traceback): + root_logger = logging.getLogger() + if self.level is not None: + root_logger.setLevel(self.orig_level) + root_logger.removeHandler(self.handler) class LogCaptureHandler(logging.StreamHandler): @@ -527,15 +528,11 @@ def __init__(self, config: Config) -> None: else: self.log_file_handler = None - self.log_cli_handler = None - - self.live_logs_context = lambda: nullcontext() - # Note that the lambda for the live_logs_context is needed because - # live_logs_context can otherwise not be entered multiple times due - # to limitations of contextlib.contextmanager. - if self._log_cli_enabled(): self._setup_cli_logging() + else: + self.log_cli_handler = None + self.live_logs_context = nullcontext def _create_formatter(self, log_format, log_date_format, auto_indent): # color option doesn't exist if terminal plugin is disabled From 075903dafa6ef6f62e2af67f336ee45f267feaa1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 209/823] logging: simplify log-file handling - Instead of making it optional, always set up a handler, but possibly going to /dev/null. This simplifies the code by removing a lot of conditionals. It also can replace the NullHandler() we already add. - Change `set_log_path` to just change the stream, instead of recreating one. Besides plugging a resource leak, it enables the next item. - Remove the capturing_logs from _runtest_for, since it sufficiently covered by the one in pytest_runtestloop now, which wraps all other _runtest_for calls. The first item alone would have had an adverse performance impact, but the last item removes it. --- src/_pytest/logging.py | 94 ++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 55 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index fe20153721c..49f2af2c796 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -1,6 +1,8 @@ """ Access and control log capturing. """ import logging +import os import re +import sys from contextlib import contextmanager from io import StringIO from typing import AbstractSet @@ -503,6 +505,7 @@ def __init__(self, config: Config) -> None: _issue_warning_captured(NO_PRINT_LOGS, self._config.hook, stacklevel=2) + # Report logging. self.formatter = self._create_formatter( get_option_ini(config, "log_format"), get_option_ini(config, "log_date_format"), @@ -510,24 +513,22 @@ def __init__(self, config: Config) -> None: ) self.log_level = get_log_level_for_setting(config, "log_level") + # File logging. self.log_file_level = get_log_level_for_setting(config, "log_file_level") - self.log_file_format = get_option_ini(config, "log_file_format", "log_format") - self.log_file_date_format = get_option_ini( + log_file = get_option_ini(config, "log_file") or os.devnull + self.log_file_handler = logging.FileHandler( + log_file, mode="w", encoding="UTF-8" + ) + log_file_format = get_option_ini(config, "log_file_format", "log_format") + log_file_date_format = get_option_ini( config, "log_file_date_format", "log_date_format" ) - self.log_file_formatter = logging.Formatter( - self.log_file_format, datefmt=self.log_file_date_format + log_file_formatter = logging.Formatter( + log_file_format, datefmt=log_file_date_format ) + self.log_file_handler.setFormatter(log_file_formatter) - log_file = get_option_ini(config, "log_file") - if log_file: - self.log_file_handler = logging.FileHandler( - log_file, mode="w", encoding="UTF-8" - ) # type: Optional[logging.FileHandler] - self.log_file_handler.setFormatter(self.log_file_formatter) - else: - self.log_file_handler = None - + # CLI/live logging. if self._log_cli_enabled(): self._setup_cli_logging() else: @@ -592,10 +593,19 @@ def set_log_path(self, fname): if not fname.parent.exists(): fname.parent.mkdir(exist_ok=True, parents=True) - self.log_file_handler = logging.FileHandler( - str(fname), mode="w", encoding="UTF-8" - ) - self.log_file_handler.setFormatter(self.log_file_formatter) + stream = fname.open(mode="w", encoding="UTF-8") + if sys.version_info >= (3, 7): + old_stream = self.log_file_handler.setStream(stream) + else: + old_stream = self.log_file_handler.stream + self.log_file_handler.acquire() + try: + self.log_file_handler.flush() + self.log_file_handler.stream = stream + finally: + self.log_file_handler.release() + if old_stream: + old_stream.close() def _log_cli_enabled(self): """Return True if log_cli should be considered enabled, either explicitly @@ -611,17 +621,8 @@ def pytest_collection(self) -> Generator[None, None, None]: if self.log_cli_handler: self.log_cli_handler.set_when("collection") - if self.log_file_handler is not None: - with catching_logs(self.log_file_handler, level=self.log_file_level): - yield - else: - # Add a dummy handler to ensure logging.root.handlers is not empty. - # If it were empty, then a `logging.warning()` call (and similar) during collection - # would trigger a `logging.basicConfig()` call, which would add a `StreamHandler` - # handler, which would cause all subsequent logs which reach the root to be also - # printed to stdout, which we don't want (issue #6240). - with catching_logs(logging.NullHandler()): - yield + with catching_logs(self.log_file_handler, level=self.log_file_level): + yield def _runtest_for( self, item: Optional[nodes.Item], when: str @@ -640,13 +641,7 @@ def _runtest_for( ] = log_handler item._store[catch_log_handler_key] = log_handler try: - if self.log_file_handler is not None: - with catching_logs( - self.log_file_handler, level=self.log_file_level - ): - yield - else: - yield + yield finally: if item is not None and when == "teardown": del item._store[catch_log_handlers_key] @@ -688,28 +683,20 @@ def pytest_sessionfinish(self): with self.live_logs_context(): if self.log_cli_handler: self.log_cli_handler.set_when("sessionfinish") - if self.log_file_handler is not None: - try: - with catching_logs( - self.log_file_handler, level=self.log_file_level - ): - yield - finally: - # Close the FileHandler explicitly. - # (logging.shutdown might have lost the weakref?!) - self.log_file_handler.close() - else: - yield + try: + with catching_logs(self.log_file_handler, level=self.log_file_level): + yield + finally: + # Close the FileHandler explicitly. + # (logging.shutdown might have lost the weakref?!) + self.log_file_handler.close() @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionstart(self): with self.live_logs_context(): if self.log_cli_handler: self.log_cli_handler.set_when("sessionstart") - if self.log_file_handler is not None: - with catching_logs(self.log_file_handler, level=self.log_file_level): - yield - else: + with catching_logs(self.log_file_handler, level=self.log_file_level): yield @pytest.hookimpl(hookwrapper=True) @@ -725,10 +712,7 @@ def pytest_runtestloop(self, session): self._config.option.verbose = 1 with self.live_logs_context(): - if self.log_file_handler is not None: - with catching_logs(self.log_file_handler, level=self.log_file_level): - yield # run all the tests - else: + with catching_logs(self.log_file_handler, level=self.log_file_level): yield # run all the tests From b13af52bbe65b4273067bdbf7656e01bfa96395d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 210/823] logging: call set_when() in a consistent manner --- src/_pytest/logging.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 49f2af2c796..3c4bd50e50e 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -617,10 +617,10 @@ def _log_cli_enabled(self): @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection(self) -> Generator[None, None, None]: - with self.live_logs_context(): - if self.log_cli_handler: - self.log_cli_handler.set_when("collection") + if self.log_cli_handler is not None: + self.log_cli_handler.set_when("collection") + with self.live_logs_context(): with catching_logs(self.log_file_handler, level=self.log_file_level): yield @@ -631,9 +631,6 @@ def _runtest_for( log_handler = LogCaptureHandler() log_handler.setFormatter(self.formatter) with catching_logs(log_handler, level=self.log_level): - if self.log_cli_handler: - self.log_cli_handler.set_when(when) - if item is not None: empty = {} # type: Dict[str, LogCaptureHandler] item._store.setdefault(catch_log_handlers_key, empty)[ @@ -654,21 +651,30 @@ def _runtest_for( @pytest.hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item): + if self.log_cli_handler is not None: + self.log_cli_handler.set_when("setup") + yield from self._runtest_for(item, "setup") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): + if self.log_cli_handler is not None: + self.log_cli_handler.set_when("call") + yield from self._runtest_for(item, "call") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item): + if self.log_cli_handler is not None: + self.log_cli_handler.set_when("teardown") + yield from self._runtest_for(item, "teardown") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_logstart(self): - if self.log_cli_handler: + if self.log_cli_handler is not None: self.log_cli_handler.reset() - yield from self._runtest_for(None, "start") + self.log_cli_handler.set_when("start") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_logfinish(self): @@ -680,9 +686,10 @@ def pytest_runtest_logreport(self): @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionfinish(self): + if self.log_cli_handler is not None: + self.log_cli_handler.set_when("sessionfinish") + with self.live_logs_context(): - if self.log_cli_handler: - self.log_cli_handler.set_when("sessionfinish") try: with catching_logs(self.log_file_handler, level=self.log_file_level): yield @@ -693,9 +700,10 @@ def pytest_sessionfinish(self): @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionstart(self): + if self.log_cli_handler is not None: + self.log_cli_handler.set_when("sessionstart") + with self.live_logs_context(): - if self.log_cli_handler: - self.log_cli_handler.set_when("sessionstart") with catching_logs(self.log_file_handler, level=self.log_file_level): yield From bd657bab3f1a90c24f9dc817e93bf8388f89bda3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 211/823] logging: don't use _runtest_for for the pytest_log* hooks The logstart/logreport/logfinish hooks don't need the stuff in _runtest_for. The test capturing catching_logs call is irrelevant for them, and the item-conditional sections are gone. --- src/_pytest/logging.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 3c4bd50e50e..00bac23df3c 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -624,27 +624,17 @@ def pytest_collection(self) -> Generator[None, None, None]: with catching_logs(self.log_file_handler, level=self.log_file_level): yield - def _runtest_for( - self, item: Optional[nodes.Item], when: str - ) -> Generator[None, None, None]: + def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]: """Implements the internals of pytest_runtest_xxx() hook.""" log_handler = LogCaptureHandler() log_handler.setFormatter(self.formatter) with catching_logs(log_handler, level=self.log_level): - if item is not None: - empty = {} # type: Dict[str, LogCaptureHandler] - item._store.setdefault(catch_log_handlers_key, empty)[ - when - ] = log_handler - item._store[catch_log_handler_key] = log_handler - try: - yield - finally: - if item is not None and when == "teardown": - del item._store[catch_log_handlers_key] - del item._store[catch_log_handler_key] + item._store[catch_log_handlers_key][when] = log_handler + item._store[catch_log_handler_key] = log_handler + + yield - if item is not None and self.print_logs: + if self.print_logs: # Add a captured log section to the report. log = log_handler.stream.getvalue().strip() item.add_report_section(when, "log", log) @@ -654,6 +644,8 @@ def pytest_runtest_setup(self, item): if self.log_cli_handler is not None: self.log_cli_handler.set_when("setup") + empty = {} # type: Dict[str, LogCaptureHandler] + item._store[catch_log_handlers_key] = empty yield from self._runtest_for(item, "setup") @pytest.hookimpl(hookwrapper=True) @@ -669,20 +661,24 @@ def pytest_runtest_teardown(self, item): self.log_cli_handler.set_when("teardown") yield from self._runtest_for(item, "teardown") + del item._store[catch_log_handlers_key] + del item._store[catch_log_handler_key] - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl def pytest_runtest_logstart(self): if self.log_cli_handler is not None: self.log_cli_handler.reset() self.log_cli_handler.set_when("start") - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl def pytest_runtest_logfinish(self): - yield from self._runtest_for(None, "finish") + if self.log_cli_handler is not None: + self.log_cli_handler.set_when("finish") - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl def pytest_runtest_logreport(self): - yield from self._runtest_for(None, "logreport") + if self.log_cli_handler is not None: + self.log_cli_handler.set_when("logreport") @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionfinish(self): From 43c465c9bf6d40bd579d62e63e883823368e1fde Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 212/823] logging: use dummy handler when CLI logging is disabled instead of None This makes the code cleaner by removing conditionals and making the CLI and file logging completely analogous. Doesn't affect performance. --- src/_pytest/logging.py | 121 ++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 62 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 00bac23df3c..eca5c240b48 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -11,6 +11,7 @@ from typing import List from typing import Mapping from typing import Optional +from typing import Union import pytest from _pytest import nodes @@ -529,11 +530,24 @@ def __init__(self, config: Config) -> None: self.log_file_handler.setFormatter(log_file_formatter) # CLI/live logging. + self.log_cli_level = get_log_level_for_setting( + config, "log_cli_level", "log_level" + ) if self._log_cli_enabled(): - self._setup_cli_logging() + terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") + capture_manager = config.pluginmanager.get_plugin("capturemanager") + # if capturemanager plugin is disabled, live logging still works. + self.log_cli_handler = _LiveLoggingStreamHandler( + terminal_reporter, capture_manager + ) # type: Union[_LiveLoggingStreamHandler, _LiveLoggingNullHandler] else: - self.log_cli_handler = None - self.live_logs_context = nullcontext + self.log_cli_handler = _LiveLoggingNullHandler() + log_cli_formatter = self._create_formatter( + get_option_ini(config, "log_cli_format", "log_format"), + get_option_ini(config, "log_cli_date_format", "log_date_format"), + get_option_ini(config, "log_auto_indent"), + ) + self.log_cli_handler.setFormatter(log_cli_formatter) def _create_formatter(self, log_format, log_date_format, auto_indent): # color option doesn't exist if terminal plugin is disabled @@ -553,30 +567,6 @@ def _create_formatter(self, log_format, log_date_format, auto_indent): return formatter - def _setup_cli_logging(self): - config = self._config - terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") - if terminal_reporter is None: - # terminal reporter is disabled e.g. by pytest-xdist. - return - - capture_manager = config.pluginmanager.get_plugin("capturemanager") - # if capturemanager plugin is disabled, live logging still works. - log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) - - log_cli_formatter = self._create_formatter( - get_option_ini(config, "log_cli_format", "log_format"), - get_option_ini(config, "log_cli_date_format", "log_date_format"), - get_option_ini(config, "log_auto_indent"), - ) - log_cli_handler.setFormatter(log_cli_formatter) - - log_cli_level = get_log_level_for_setting(config, "log_cli_level", "log_level") - self.log_cli_handler = log_cli_handler - self.live_logs_context = lambda: catching_logs( - log_cli_handler, level=log_cli_level - ) - def set_log_path(self, fname): """Public method, which can set filename parameter for Logging.FileHandler(). Also creates parent directory if @@ -608,19 +598,25 @@ def set_log_path(self, fname): old_stream.close() def _log_cli_enabled(self): - """Return True if log_cli should be considered enabled, either explicitly - or because --log-cli-level was given in the command-line. - """ - return self._config.getoption( + """Return whether live logging is enabled.""" + enabled = self._config.getoption( "--log-cli-level" ) is not None or self._config.getini("log_cli") + if not enabled: + return False + + terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") + if terminal_reporter is None: + # terminal reporter is disabled e.g. by pytest-xdist. + return False + + return True @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection(self) -> Generator[None, None, None]: - if self.log_cli_handler is not None: - self.log_cli_handler.set_when("collection") + self.log_cli_handler.set_when("collection") - with self.live_logs_context(): + 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 @@ -641,8 +637,7 @@ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, Non @pytest.hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item): - if self.log_cli_handler is not None: - self.log_cli_handler.set_when("setup") + self.log_cli_handler.set_when("setup") empty = {} # type: Dict[str, LogCaptureHandler] item._store[catch_log_handlers_key] = empty @@ -650,15 +645,13 @@ def pytest_runtest_setup(self, item): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): - if self.log_cli_handler is not None: - self.log_cli_handler.set_when("call") + self.log_cli_handler.set_when("call") yield from self._runtest_for(item, "call") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item): - if self.log_cli_handler is not None: - self.log_cli_handler.set_when("teardown") + self.log_cli_handler.set_when("teardown") yield from self._runtest_for(item, "teardown") del item._store[catch_log_handlers_key] @@ -666,40 +659,34 @@ def pytest_runtest_teardown(self, item): @pytest.hookimpl def pytest_runtest_logstart(self): - if self.log_cli_handler is not None: - self.log_cli_handler.reset() - self.log_cli_handler.set_when("start") + self.log_cli_handler.reset() + self.log_cli_handler.set_when("start") @pytest.hookimpl def pytest_runtest_logfinish(self): - if self.log_cli_handler is not None: - self.log_cli_handler.set_when("finish") + self.log_cli_handler.set_when("finish") @pytest.hookimpl def pytest_runtest_logreport(self): - if self.log_cli_handler is not None: - self.log_cli_handler.set_when("logreport") + self.log_cli_handler.set_when("logreport") @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionfinish(self): - if self.log_cli_handler is not None: - self.log_cli_handler.set_when("sessionfinish") + self.log_cli_handler.set_when("sessionfinish") - with self.live_logs_context(): - try: - with catching_logs(self.log_file_handler, level=self.log_file_level): - yield - finally: - # Close the FileHandler explicitly. - # (logging.shutdown might have lost the weakref?!) - self.log_file_handler.close() + 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 + + # Close the FileHandler explicitly. + # (logging.shutdown might have lost the weakref?!) + self.log_file_handler.close() @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionstart(self): - if self.log_cli_handler is not None: - self.log_cli_handler.set_when("sessionstart") + self.log_cli_handler.set_when("sessionstart") - with self.live_logs_context(): + 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 @@ -715,7 +702,7 @@ def pytest_runtestloop(self, session): # setting verbose flag is needed to avoid messy test progress output self._config.option.verbose = 1 - with self.live_logs_context(): + 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 @@ -768,4 +755,14 @@ def emit(self, record): if not self._section_name_shown and self._when: self.stream.section("live log " + self._when, sep="-", bold=True) self._section_name_shown = True - logging.StreamHandler.emit(self, record) + super().emit(record) + + +class _LiveLoggingNullHandler(logging.NullHandler): + """A handler used when live logging is disabled.""" + + def reset(self): + pass + + def set_when(self, when): + pass From bd5e3f042d9b38bc4b37bd4bbd477fcf76a87383 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 213/823] logging: move log_file_handler cleanup from sessionend to unconfigure It is set-up in configure, so match it. --- src/_pytest/logging.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index eca5c240b48..39523d0a3ba 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -678,10 +678,6 @@ def pytest_sessionfinish(self): with catching_logs(self.log_file_handler, level=self.log_file_level): yield - # Close the FileHandler explicitly. - # (logging.shutdown might have lost the weakref?!) - self.log_file_handler.close() - @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionstart(self): self.log_cli_handler.set_when("sessionstart") @@ -706,6 +702,12 @@ def pytest_runtestloop(self, session): with catching_logs(self.log_file_handler, level=self.log_file_level): yield # run all the tests + @pytest.hookimpl + def pytest_unconfigure(self): + # Close the FileHandler explicitly. + # (logging.shutdown might have lost the weakref?!) + self.log_file_handler.close() + class _LiveLoggingStreamHandler(logging.StreamHandler): """ From 3f8200676f12846b74289f9b2e35747623fc768a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 214/823] logging: remove deprecated --no-print-logs option/ini This option was deprecated in 5.4.0 and was marked for removal in 6.0.0. --- changelog/7224.breaking.rst | 2 ++ doc/en/deprecations.rst | 4 +-- src/_pytest/deprecated.py | 5 --- src/_pytest/logging.py | 22 ++----------- testing/deprecated_test.py | 41 ----------------------- testing/logging/test_reporting.py | 54 ------------------------------- 6 files changed, 6 insertions(+), 122 deletions(-) diff --git a/changelog/7224.breaking.rst b/changelog/7224.breaking.rst index 32ab2c0734d..32592695aef 100644 --- a/changelog/7224.breaking.rst +++ b/changelog/7224.breaking.rst @@ -1,2 +1,4 @@ The `item.catch_log_handler` and `item.catch_log_handlers` attributes, set by the logging plugin and never meant to be public , are no longer available. + +The deprecated ``--no-print-logs`` option is removed. Use ``--show-capture`` instead. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index fbcaf2ce446..0b7d3fecdab 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -37,10 +37,10 @@ a public API and may break in the future. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 5.4 +.. versionremoved:: 6.0 -Option ``--no-print-logs`` is deprecated and meant to be removed in a future release. If you use ``--no-print-logs``, please try out ``--show-capture`` and -provide feedback. +Option ``--no-print-logs`` is removed. If you used ``--no-print-logs``, please use ``--show-capture`` instead. ``--show-capture`` command-line option was added in ``pytest 3.5.0`` and allows to specify how to display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default). diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 9f4570f85b0..f981a4a4b9e 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -54,11 +54,6 @@ "for more information." ) -NO_PRINT_LOGS = PytestDeprecationWarning( - "--no-print-logs is deprecated and scheduled for removal in pytest 6.0.\n" - "Please use --show-capture instead." -) - COLLECT_DIRECTORY_HOOK = PytestDeprecationWarning( "The pytest_collect_directory hook is not working.\n" "Please use collect_ignore in conftests or pytest_collection_modifyitems." diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 39523d0a3ba..d875879a9a4 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -190,15 +190,6 @@ def add_option_ini(option, dest, default=None, type=None, **kwargs): ) group.addoption(option, dest=dest, **kwargs) - add_option_ini( - "--no-print-logs", - dest="log_print", - action="store_const", - const=False, - default=True, - type="bool", - help="disable printing caught logs on failed tests.", - ) add_option_ini( "--log-level", dest="log_level", @@ -499,13 +490,6 @@ def __init__(self, config: Config) -> None: """ self._config = config - self.print_logs = get_option_ini(config, "log_print") - if not self.print_logs: - from _pytest.warnings import _issue_warning_captured - from _pytest.deprecated import NO_PRINT_LOGS - - _issue_warning_captured(NO_PRINT_LOGS, self._config.hook, stacklevel=2) - # Report logging. self.formatter = self._create_formatter( get_option_ini(config, "log_format"), @@ -630,10 +614,8 @@ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, Non yield - if self.print_logs: - # Add a captured log section to the report. - log = log_handler.stream.getvalue().strip() - item.add_report_section(when, "log", log) + log = log_handler.stream.getvalue().strip() + item.add_report_section(when, "log", log) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item): diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index edd5505c03d..93264f3fc61 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -117,47 +117,6 @@ class MockConfig: assert w[0].filename == __file__ -def assert_no_print_logs(testdir, args): - result = testdir.runpytest(*args) - result.stdout.fnmatch_lines( - [ - "*--no-print-logs is deprecated and scheduled for removal in pytest 6.0*", - "*Please use --show-capture instead.*", - ] - ) - - -@pytest.mark.filterwarnings("default") -def test_noprintlogs_is_deprecated_cmdline(testdir): - testdir.makepyfile( - """ - def test_foo(): - pass - """ - ) - - assert_no_print_logs(testdir, ("--no-print-logs",)) - - -@pytest.mark.filterwarnings("default") -def test_noprintlogs_is_deprecated_ini(testdir): - testdir.makeini( - """ - [pytest] - log_print=False - """ - ) - - testdir.makepyfile( - """ - def test_foo(): - pass - """ - ) - - assert_no_print_logs(testdir, ()) - - def test__fillfuncargs_is_deprecated() -> None: with pytest.warns( pytest.PytestDeprecationWarning, diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index ad7af937008..c1335b180d3 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -166,60 +166,6 @@ def teardown_function(function): ) -def test_disable_log_capturing(testdir): - testdir.makepyfile( - """ - import sys - import logging - - logger = logging.getLogger(__name__) - - def test_foo(): - sys.stdout.write('text going to stdout') - logger.warning('catch me if you can!') - sys.stderr.write('text going to stderr') - assert False - """ - ) - result = testdir.runpytest("--no-print-logs") - print(result.stdout) - assert result.ret == 1 - result.stdout.fnmatch_lines(["*- Captured stdout call -*", "text going to stdout"]) - result.stdout.fnmatch_lines(["*- Captured stderr call -*", "text going to stderr"]) - with pytest.raises(pytest.fail.Exception): - result.stdout.fnmatch_lines(["*- Captured *log call -*"]) - - -def test_disable_log_capturing_ini(testdir): - testdir.makeini( - """ - [pytest] - log_print=False - """ - ) - testdir.makepyfile( - """ - import sys - import logging - - logger = logging.getLogger(__name__) - - def test_foo(): - sys.stdout.write('text going to stdout') - logger.warning('catch me if you can!') - sys.stderr.write('text going to stderr') - assert False - """ - ) - result = testdir.runpytest() - print(result.stdout) - assert result.ret == 1 - result.stdout.fnmatch_lines(["*- Captured stdout call -*", "text going to stdout"]) - result.stdout.fnmatch_lines(["*- Captured stderr call -*", "text going to stderr"]) - with pytest.raises(pytest.fail.Exception): - result.stdout.fnmatch_lines(["*- Captured *log call -*"]) - - @pytest.mark.parametrize("enabled", [True, False]) def test_log_cli_enabled_disabled(testdir, enabled): msg = "critical message logged by test" From f71ec8cc907ad8f105631abdc6fd3aff365fb887 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 May 2020 14:58:04 +0300 Subject: [PATCH 215/823] logging: order hookimpl's in chronological order Makes it easier to understand what's going on. --- src/_pytest/logging.py | 66 +++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index d875879a9a4..f69bcba8980 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -596,6 +596,14 @@ def _log_cli_enabled(self): return True + @pytest.hookimpl(hookwrapper=True, tryfirst=True) + def pytest_sessionstart(self): + 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 + @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("collection") @@ -604,6 +612,31 @@ def pytest_collection(self) -> Generator[None, None, None]: with catching_logs(self.log_file_handler, level=self.log_file_level): yield + @pytest.hookimpl(hookwrapper=True) + def pytest_runtestloop(self, session): + """Runs all collected test items.""" + + if session.config.option.collectonly: + yield + return + + if self._log_cli_enabled() and self._config.getoption("verbose") < 1: + # setting verbose flag is needed to avoid messy test progress output + self._config.option.verbose = 1 + + 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 + + @pytest.hookimpl + def pytest_runtest_logstart(self): + self.log_cli_handler.reset() + self.log_cli_handler.set_when("start") + + @pytest.hookimpl + def pytest_runtest_logreport(self): + self.log_cli_handler.set_when("logreport") + def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]: """Implements the internals of pytest_runtest_xxx() hook.""" log_handler = LogCaptureHandler() @@ -639,19 +672,10 @@ def pytest_runtest_teardown(self, item): del item._store[catch_log_handlers_key] del item._store[catch_log_handler_key] - @pytest.hookimpl - def pytest_runtest_logstart(self): - self.log_cli_handler.reset() - self.log_cli_handler.set_when("start") - @pytest.hookimpl def pytest_runtest_logfinish(self): self.log_cli_handler.set_when("finish") - @pytest.hookimpl - def pytest_runtest_logreport(self): - self.log_cli_handler.set_when("logreport") - @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionfinish(self): self.log_cli_handler.set_when("sessionfinish") @@ -660,30 +684,6 @@ def pytest_sessionfinish(self): with catching_logs(self.log_file_handler, level=self.log_file_level): yield - @pytest.hookimpl(hookwrapper=True, tryfirst=True) - def pytest_sessionstart(self): - 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 - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtestloop(self, session): - """Runs all collected test items.""" - - if session.config.option.collectonly: - yield - return - - if self._log_cli_enabled() and self._config.getoption("verbose") < 1: - # setting verbose flag is needed to avoid messy test progress output - self._config.option.verbose = 1 - - 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 - @pytest.hookimpl def pytest_unconfigure(self): # Close the FileHandler explicitly. From 18bc706fdc55f9e6636670550cb34c9b64af27a7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 2 Apr 2020 15:25:53 +0200 Subject: [PATCH 216/823] tests: revisit tests for removed load_module The tests came via c629f6b18 and c61ff31ffa1. The fixes from there are kind of obsoleted by 4cd08f9 (moving to importlib), but it makes sense to keep them as integration tests in general. --- testing/test_assertrewrite.py | 60 +++++++---------------------------- 1 file changed, 11 insertions(+), 49 deletions(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index a045eda2eaf..7bc853e829e 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1028,74 +1028,36 @@ def test_read_pyc(self, tmpdir): assert _read_pyc(str(source), str(pyc)) is None # no error - def test_reload_is_same(self, testdir): - # A file that will be picked up during collecting. - testdir.tmpdir.join("file.py").ensure() - testdir.tmpdir.join("pytest.ini").write( - textwrap.dedent( - """ + def test_reload_is_same_and_reloads(self, testdir: Testdir) -> None: + """Reloading a (collected) module after change picks up the change.""" + testdir.makeini( + """ [pytest] python_files = *.py - """ - ) - ) - - testdir.makepyfile( - test_fun=""" - import sys - try: - from imp import reload - except ImportError: - pass - - def test_loader(): - import file - assert sys.modules["file"] is reload(file) """ ) - result = testdir.runpytest("-s") - result.stdout.fnmatch_lines(["* 1 passed*"]) - - def test_reload_reloads(self, testdir): - """Reloading a module after change picks up the change.""" - testdir.tmpdir.join("file.py").write( - textwrap.dedent( - """ + testdir.makepyfile( + file=""" def reloaded(): return False def rewrite_self(): with open(__file__, 'w') as self: self.write('def reloaded(): return True') - """ - ) - ) - testdir.tmpdir.join("pytest.ini").write( - textwrap.dedent( - """ - [pytest] - python_files = *.py - """ - ) - ) - - testdir.makepyfile( + """, test_fun=""" import sys - try: - from imp import reload - except ImportError: - pass + from importlib import reload def test_loader(): import file assert not file.reloaded() file.rewrite_self() - reload(file) + assert sys.modules["file"] is reload(file) assert file.reloaded() - """ + """, ) - result = testdir.runpytest("-s") + result = testdir.runpytest() result.stdout.fnmatch_lines(["* 1 passed*"]) def test_get_data_support(self, testdir): From d2d11a8bdcd382b0ac1e75af0a36cf826bcc3fa0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 13 May 2020 01:29:25 +0300 Subject: [PATCH 217/823] logging: reuse LoggingCaptureHandler instance since it's expensive to create Previously, a LoggingCaptureHandler was instantiated for each test's setup/call/teardown which turns out to be expensive. Instead, only keep one instance and reset it between runs. --- src/_pytest/logging.py | 22 +++++++++------------- testing/logging/test_fixture.py | 4 ++-- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index f69bcba8980..e2f691a310a 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -27,7 +27,7 @@ DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" _ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m") catch_log_handler_key = StoreKey["LogCaptureHandler"]() -catch_log_handlers_key = StoreKey[Dict[str, "LogCaptureHandler"]]() +catch_log_records_key = StoreKey[Dict[str, List[logging.LogRecord]]]() def _remove_ansi_escape_sequences(text): @@ -351,11 +351,7 @@ def get_records(self, when: str) -> List[logging.LogRecord]: .. versionadded:: 3.4 """ - handler = self._item._store[catch_log_handlers_key].get(when) - if handler: - return handler.records - else: - return [] + return self._item._store[catch_log_records_key].get(when, []) @property def text(self): @@ -497,6 +493,8 @@ def __init__(self, config: Config) -> None: get_option_ini(config, "log_auto_indent"), ) self.log_level = get_log_level_for_setting(config, "log_level") + self.log_handler = LogCaptureHandler() + self.log_handler.setFormatter(self.formatter) # File logging. self.log_file_level = get_log_level_for_setting(config, "log_file_level") @@ -639,10 +637,9 @@ def pytest_runtest_logreport(self): def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]: """Implements the internals of pytest_runtest_xxx() hook.""" - log_handler = LogCaptureHandler() - log_handler.setFormatter(self.formatter) - with catching_logs(log_handler, level=self.log_level): - item._store[catch_log_handlers_key][when] = log_handler + with catching_logs(self.log_handler, level=self.log_level) as log_handler: + log_handler.reset() + item._store[catch_log_records_key][when] = log_handler.records item._store[catch_log_handler_key] = log_handler yield @@ -654,8 +651,7 @@ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, Non def pytest_runtest_setup(self, item): self.log_cli_handler.set_when("setup") - empty = {} # type: Dict[str, LogCaptureHandler] - item._store[catch_log_handlers_key] = empty + item._store[catch_log_records_key] = {} yield from self._runtest_for(item, "setup") @pytest.hookimpl(hookwrapper=True) @@ -669,7 +665,7 @@ def pytest_runtest_teardown(self, item): self.log_cli_handler.set_when("teardown") yield from self._runtest_for(item, "teardown") - del item._store[catch_log_handlers_key] + del item._store[catch_log_records_key] del item._store[catch_log_handler_key] @pytest.hookimpl diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index a33b0e80ecd..657ffb4ddf2 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -1,7 +1,7 @@ import logging import pytest -from _pytest.logging import catch_log_handlers_key +from _pytest.logging import catch_log_records_key logger = logging.getLogger(__name__) sublogger = logging.getLogger(__name__ + ".baz") @@ -137,4 +137,4 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"] # This reaches into private API, don't use this type of thing in real tests! - assert set(caplog._item._store[catch_log_handlers_key]) == {"setup", "call"} + assert set(caplog._item._store[catch_log_records_key]) == {"setup", "call"} From 8b9b81c3c04399d0ebde8d85a9db60291b325acd Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 May 2020 19:08:47 +0200 Subject: [PATCH 218/823] Function: use `originalname` in `_getobj` and make it default to `name` (#7035) --- changelog/7035.trivial.rst | 2 ++ src/_pytest/python.py | 42 ++++++++++++++++++++++++++++---------- testing/python/collect.py | 28 +++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 changelog/7035.trivial.rst diff --git a/changelog/7035.trivial.rst b/changelog/7035.trivial.rst new file mode 100644 index 00000000000..076cb4b4bcd --- /dev/null +++ b/changelog/7035.trivial.rst @@ -0,0 +1,2 @@ +The ``originalname`` attribute of ``_pytest.python.Function`` now defaults to ``name`` if not +provided explicitly, and is always set. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index d3acfad4458..c943824fe6b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1395,11 +1395,41 @@ def __init__( fixtureinfo: Optional[FuncFixtureInfo] = None, originalname=None, ) -> None: + """ + param name: the full function name, including any decorations like those + added by parametrization (``my_func[my_param]``). + param parent: the parent Node. + param args: (unused) + param config: the pytest Config object + param callspec: if given, this is function has been parametrized and the callspec contains + meta information about the parametrization. + param callobj: if given, the object which will be called when the Function is invoked, + otherwise the callobj will be obtained from ``parent`` using ``originalname`` + param keywords: keywords bound to the function object for "-k" matching. + param session: the pytest Session object + param fixtureinfo: fixture information already resolved at this fixture node. + param originalname: + The attribute name to use for accessing the underlying function object. + Defaults to ``name``. Set this if name is different from the original name, + for example when it contains decorations like those added by parametrization + (``my_func[my_param]``). + """ super().__init__(name, parent, config=config, session=session) self._args = args if callobj is not NOTSET: self.obj = callobj + #: Original function name, without any decorations (for example + #: parametrization adds a ``"[...]"`` suffix to function names), used to access + #: the underlying function object from ``parent`` (in case ``callobj`` is not given + #: explicitly). + #: + #: .. versionadded:: 3.0 + self.originalname = originalname or name + + # note: when FunctionDefinition is introduced, we should change ``originalname`` + # to a readonly property that returns FunctionDefinition.name + self.keywords.update(self.obj.__dict__) self.own_markers.extend(get_unpacked_marks(self.obj)) if callspec: @@ -1434,12 +1464,6 @@ def __init__( self.fixturenames = fixtureinfo.names_closure self._initrequest() - #: original function name, without any decorations (for example - #: parametrization adds a ``"[...]"`` suffix to function names). - #: - #: .. versionadded:: 3.0 - self.originalname = originalname - @classmethod def from_parent(cls, parent, **kw): # todo: determine sound type limitations """ @@ -1457,11 +1481,7 @@ def function(self): return getimfunc(self.obj) def _getobj(self): - name = self.name - i = name.find("[") # parametrization - if i != -1: - name = name[:i] - return getattr(self.parent.obj, name) + return getattr(self.parent.obj, self.originalname) @property def _pyfuncitem(self): diff --git a/testing/python/collect.py b/testing/python/collect.py index 496a22b0504..94441878c23 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -6,6 +6,7 @@ import pytest from _pytest.config import ExitCode from _pytest.nodes import Collector +from _pytest.pytester import Testdir class TestModule: @@ -659,16 +660,39 @@ def test_passed(x): result = testdir.runpytest() result.stdout.fnmatch_lines(["* 3 passed in *"]) - def test_function_original_name(self, testdir): + def test_function_originalname(self, testdir: Testdir) -> None: items = testdir.getitems( """ import pytest + @pytest.mark.parametrize('arg', [1,2]) def test_func(arg): pass + + def test_no_param(): + pass """ ) - assert [x.originalname for x in items] == ["test_func", "test_func"] + assert [x.originalname for x in items] == [ + "test_func", + "test_func", + "test_no_param", + ] + + def test_function_with_square_brackets(self, testdir: Testdir) -> None: + """Check that functions with square brackets don't cause trouble.""" + p1 = testdir.makepyfile( + """ + locals()["test_foo[name]"] = lambda: None + """ + ) + result = testdir.runpytest("-v", str(p1)) + result.stdout.fnmatch_lines( + [ + "test_function_with_square_brackets.py::test_foo[[]name[]] PASSED *", + "*= 1 passed in *", + ] + ) class TestSorting: From ad3169428bc9fe3fb404c82b78c84f67a6011ec1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 18 May 2020 14:20:13 -0300 Subject: [PATCH 219/823] Remove unused Function.__init__ 'args' parameter --- changelog/7226.breaking.rst | 1 + src/_pytest/python.py | 4 +--- testing/python/collect.py | 6 ++++-- 3 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog/7226.breaking.rst diff --git a/changelog/7226.breaking.rst b/changelog/7226.breaking.rst new file mode 100644 index 00000000000..bf1c443fc18 --- /dev/null +++ b/changelog/7226.breaking.rst @@ -0,0 +1 @@ +Removed the unused ``args`` parameter from ``pytest.Function.__init__``. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c943824fe6b..0224a2a8984 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1386,7 +1386,6 @@ def __init__( self, name, parent, - args=None, config=None, callspec: Optional[CallSpec2] = None, callobj=NOTSET, @@ -1399,7 +1398,6 @@ def __init__( param name: the full function name, including any decorations like those added by parametrization (``my_func[my_param]``). param parent: the parent Node. - param args: (unused) param config: the pytest Config object param callspec: if given, this is function has been parametrized and the callspec contains meta information about the parametrization. @@ -1415,7 +1413,7 @@ def __init__( (``my_func[my_param]``). """ super().__init__(name, parent, config=config, session=session) - self._args = args + if callobj is not NOTSET: self.obj = callobj diff --git a/testing/python/collect.py b/testing/python/collect.py index 94441878c23..2807cacc90a 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -294,9 +294,11 @@ def func1(): def func2(): pass - f1 = self.make_function(testdir, name="name", args=(1,), callobj=func1) + f1 = self.make_function(testdir, name="name", callobj=func1) assert f1 == f1 - f2 = self.make_function(testdir, name="name", callobj=func2) + f2 = self.make_function( + testdir, name="name", callobj=func2, originalname="foobar" + ) assert f1 != f2 def test_repr_produces_actual_test_id(self, testdir): From 9bf28853bfb7486ca012e005be47546402cf7e7f Mon Sep 17 00:00:00 2001 From: Katrin Leinweber <9948149+katrinleinweber@users.noreply.github.com> Date: Mon, 18 May 2020 19:51:22 +0200 Subject: [PATCH 220/823] doc: highlight difference between progress percentage & code coverage (#6686) --- doc/en/getting-started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 06279a87a9f..2549e8ff62d 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -73,7 +73,7 @@ That’s it. You can now execute the test function: FAILED test_sample.py::test_answer - assert 4 == 5 ============================ 1 failed in 0.12s ============================= -This test returns a failure report because ``func(3)`` does not return ``5``. +The ``[100%]`` refers to the overall progress of running all test cases. After it finishes, pytest then shows a failure report because ``func(3)`` does not return ``5``. .. note:: From 694fdc655436b6b2eb335d9019002ab81486e4d8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 5 Apr 2020 12:38:37 +0300 Subject: [PATCH 221/823] Remove pyobj_property helper, inline it instead It doesn't save much code but adds indirection which makes it a bit harder to follow and to type. --- src/_pytest/python.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index d3acfad4458..099eecdf9fb 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -57,18 +57,6 @@ from _pytest.warning_types import PytestUnhandledCoroutineWarning -def pyobj_property(name): - def get(self): - node = self.getparent(getattr(__import__("pytest"), name)) - if node is not None: - return node.obj - - doc = "python {} object this node was collected from (can be None).".format( - name.lower() - ) - return property(get, None, None, doc) - - def pytest_addoption(parser): group = parser.getgroup("general") group.addoption( @@ -248,11 +236,26 @@ def pytest_pycollect_makeitem(collector, name, obj): class PyobjMixin: - module = pyobj_property("Module") - cls = pyobj_property("Class") - instance = pyobj_property("Instance") _ALLOW_MARKERS = True + @property + def module(self): + """Python module object this node was collected from (can be None).""" + node = self.getparent(Module) + return node.obj if node is not None else None + + @property + def cls(self): + """Python class object this node was collected from (can be None).""" + node = self.getparent(Class) + return node.obj if node is not None else None + + @property + def instance(self): + """Python instance object this node was collected from (can be None).""" + node = self.getparent(Instance) + return node.obj if node is not None else None + @property def obj(self): """Underlying Python object.""" From b13fcb23d79b3f38e497824c438c926a0a015561 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 19 May 2020 10:41:33 +0300 Subject: [PATCH 222/823] logging: propagate errors during log message emits Currently, a bad logging call, e.g. logger.info('oops', 'first', 2) triggers the default logging handling, which is printing an error to stderr but otherwise continuing. For regular programs this behavior makes sense, a bad log message shouldn't take down the program. But during tests, it is better not to skip over such mistakes, but propagate them to the user. --- changelog/6433.feature.rst | 10 +++++++ src/_pytest/logging.py | 30 ++++++++++++++++++-- testing/logging/test_reporting.py | 46 +++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 changelog/6433.feature.rst diff --git a/changelog/6433.feature.rst b/changelog/6433.feature.rst new file mode 100644 index 00000000000..c331b0f5888 --- /dev/null +++ b/changelog/6433.feature.rst @@ -0,0 +1,10 @@ +If an error is encountered while formatting the message in a logging call, for +example ``logging.warning("oh no!: %s: %s", "first")`` (a second argument is +missing), pytest now propagates the error, likely causing the test to fail. + +Previously, such a mistake would cause an error to be printed to stderr, which +is not displayed by default for passing tests. This change makes the mistake +visible during testing. + +You may supress this behavior temporarily or permanently by setting +``logging.raiseExceptions = False``. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index e2f691a310a..f6a2063271f 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -312,6 +312,14 @@ def reset(self) -> None: self.records = [] self.stream = StringIO() + def handleError(self, record: logging.LogRecord) -> None: + if logging.raiseExceptions: + # Fail the test if the log message is bad (emit failed). + # The default behavior of logging is to print "Logging error" + # to stderr with the call stack and some extra details. + # pytest wants to make such mistakes visible during testing. + raise + class LogCaptureFixture: """Provides access and control of log capturing.""" @@ -499,9 +507,7 @@ def __init__(self, config: Config) -> None: # File logging. self.log_file_level = get_log_level_for_setting(config, "log_file_level") log_file = get_option_ini(config, "log_file") or os.devnull - self.log_file_handler = logging.FileHandler( - log_file, mode="w", encoding="UTF-8" - ) + self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8") log_file_format = get_option_ini(config, "log_file_format", "log_format") log_file_date_format = get_option_ini( config, "log_file_date_format", "log_date_format" @@ -687,6 +693,16 @@ def pytest_unconfigure(self): self.log_file_handler.close() +class _FileHandler(logging.FileHandler): + """ + Custom FileHandler with pytest tweaks. + """ + + def handleError(self, record: logging.LogRecord) -> None: + # Handled by LogCaptureHandler. + pass + + class _LiveLoggingStreamHandler(logging.StreamHandler): """ Custom StreamHandler used by the live logging feature: it will write a newline before the first log message @@ -737,6 +753,10 @@ def emit(self, record): self._section_name_shown = True super().emit(record) + def handleError(self, record: logging.LogRecord) -> None: + # Handled by LogCaptureHandler. + pass + class _LiveLoggingNullHandler(logging.NullHandler): """A handler used when live logging is disabled.""" @@ -746,3 +766,7 @@ def reset(self): def set_when(self, when): pass + + def handleError(self, record: logging.LogRecord) -> None: + # Handled by LogCaptureHandler. + pass diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index c1335b180d3..709df2b57b0 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -3,6 +3,7 @@ import re import pytest +from _pytest.pytester import Testdir def test_nothing_logged(testdir): @@ -1101,3 +1102,48 @@ def test_foo(caplog): ) result = testdir.runpytest("--log-level=INFO", "--color=yes") assert result.ret == 0 + + +def test_logging_emit_error(testdir: Testdir) -> None: + """ + An exception raised during emit() should fail the test. + + The default behavior of logging is to print "Logging error" + to stderr with the call stack and some extra details. + + pytest overrides this behavior to propagate the exception. + """ + testdir.makepyfile( + """ + import logging + + def test_bad_log(): + logging.warning('oops', 'first', 2) + """ + ) + result = testdir.runpytest() + result.assert_outcomes(failed=1) + result.stdout.fnmatch_lines( + [ + "====* FAILURES *====", + "*not all arguments converted during string formatting*", + ] + ) + + +def test_logging_emit_error_supressed(testdir: Testdir) -> None: + """ + If logging is configured to silently ignore errors, pytest + doesn't propagate errors either. + """ + testdir.makepyfile( + """ + import logging + + def test_bad_log(monkeypatch): + monkeypatch.setattr(logging, 'raiseExceptions', False) + logging.warning('oops', 'first', 2) + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=1) From 61180eec938e41989a443ba899a55a8f6c87a8f9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 28 Feb 2020 20:32:33 +0100 Subject: [PATCH 223/823] Test behavior of Source with regard to decorators Unlinke `inspect.getsource` it does not unwrap functions. --- testing/code/test_source.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 792b8d6b160..0d01e73d27a 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -624,6 +624,28 @@ def test_comment_in_statement() -> None: ) +def test_source_with_decorator() -> None: + """Test behavior with Source / Code().source with regard to decorators.""" + from _pytest.compat import get_real_func + + @pytest.mark.foo + def deco_mark(): + pass + + src = inspect.getsource(deco_mark) + assert str(Source(deco_mark, deindent=False)) == src + assert src.startswith(" @pytest.mark.foo") + + @pytest.fixture + def deco_fixture(): + pass + + src = inspect.getsource(deco_fixture) + assert src == " @pytest.fixture\n def deco_fixture():\n pass\n" + assert str(Source(deco_fixture)).startswith("@functools.wraps(function)") + assert str(Source(get_real_func(deco_fixture), deindent=False)) == src + + def test_single_line_else() -> None: source = getstatement(1, "if False: 2\nelse: 3") assert str(source) == "else: 3" From b98a182aa1614a43c7a884148500d85d58150c69 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 28 Feb 2020 20:45:02 +0100 Subject: [PATCH 224/823] (no) coverage --- testing/code/test_source.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 0d01e73d27a..7b828abc9e4 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -630,7 +630,7 @@ def test_source_with_decorator() -> None: @pytest.mark.foo def deco_mark(): - pass + assert False src = inspect.getsource(deco_mark) assert str(Source(deco_mark, deindent=False)) == src @@ -638,10 +638,10 @@ def deco_mark(): @pytest.fixture def deco_fixture(): - pass + assert False src = inspect.getsource(deco_fixture) - assert src == " @pytest.fixture\n def deco_fixture():\n pass\n" + assert src == " @pytest.fixture\n def deco_fixture():\n assert False\n" assert str(Source(deco_fixture)).startswith("@functools.wraps(function)") assert str(Source(get_real_func(deco_fixture), deindent=False)) == src From 55099e57c37778fcb585ecdb2f1469ac7131a1f9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 19 May 2020 19:19:53 -0300 Subject: [PATCH 225/823] Add requested comment as per review --- testing/code/test_source.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 7b828abc9e4..35728c33443 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -642,6 +642,9 @@ def deco_fixture(): src = inspect.getsource(deco_fixture) assert src == " @pytest.fixture\n def deco_fixture():\n assert False\n" + # currenly Source does not unwrap decorators, testing the + # existing behavior here for explicitness, but perhaps we should revisit/change this + # in the future assert str(Source(deco_fixture)).startswith("@functools.wraps(function)") assert str(Source(get_real_func(deco_fixture), deindent=False)) == src From 87423d3cc8e3b19a7c332b8d76d407a2550edf06 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 25 Feb 2020 15:31:01 +0100 Subject: [PATCH 226/823] Keep explicit newlines with help texts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes a difference for e.g. pytest-xdist: Before: ``` --dist=distmode set mode for distributing tests to exec environments. each: … available environment. loadscope: … grouped by file to any available environment. (default) no: … ``` After: ``` --dist=distmode set mode for distributing tests to exec environments. each: send each test to all available environments. load: load balance by sending any pending test to any available environment. … (default) no: run tests inprocess, don't distribute. ``` This might also result in unexpected changes (hard wrapping), when line endings where used unintentionally, e.g. with: ``` help=""" some long help text """ ``` But the benefits from that are worth it, and it is easy to fix, as will be done for the internal `assertmode` option. --- src/_pytest/config/argparsing.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index b57db92caac..985a3fd1cd0 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -509,3 +509,15 @@ def _format_action_invocation(self, action: argparse.Action) -> str: formatted_action_invocation = ", ".join(return_list) action._formatted_action_invocation = formatted_action_invocation # type: ignore return formatted_action_invocation + + def _split_lines(self, text, width): + """Wrap lines after splitting on original newlines. + + This allows to have explicit line breaks in the help text. + """ + import textwrap + + lines = [] + for line in text.splitlines(): + lines.extend(textwrap.wrap(line.strip(), width)) + return lines From 691a7fceea5188625689aea61bb78a12ea1e3571 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 25 Feb 2020 15:46:53 +0100 Subject: [PATCH 227/823] Revisit some help texts with regard to newlines --- src/_pytest/assertion/__init__.py | 11 ++++++----- src/_pytest/cacheprovider.py | 4 ++-- src/_pytest/helpconfig.py | 2 +- src/_pytest/mark/__init__.py | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 7b5a5889d6e..b38c6c00660 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -27,11 +27,12 @@ def pytest_addoption(parser): choices=("rewrite", "plain"), default="rewrite", metavar="MODE", - help="""Control assertion debugging tools. 'plain' - performs no assertion debugging. 'rewrite' - (the default) rewrites assert statements in - test modules on import to provide assert - expression information.""", + help=( + "Control assertion debugging tools.\n" + "'plain' performs no assertion debugging.\n" + "'rewrite' (the default) rewrites assert statements in test modules" + " on import to provide assert expression information." + ), ) parser.addini( "enable_assertion_pass_hook", diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index ce820ca2b90..511ee2acfa1 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -397,9 +397,9 @@ def pytest_addoption(parser): "--failed-first", action="store_true", dest="failedfirst", - help="run all tests but run the last failures first. " + help="run all tests, but run the last failures first.\n" "This may re-order tests and thus lead to " - "repeated fixture setup/teardown", + "repeated fixture setup/teardown.", ) group.addoption( "--nf", diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index ae37fdea45b..11fd024628c 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -57,7 +57,7 @@ def pytest_addoption(parser): dest="plugins", default=[], metavar="name", - help="early-load given plugin module name or entry point (multi-allowed). " + help="early-load given plugin module name or entry point (multi-allowed).\n" "To avoid loading of plugins, use the `no:` prefix, e.g. " "`no:doctest`.", ) diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 134ed187641..242b1e0cacb 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -79,8 +79,8 @@ def pytest_addoption(parser): dest="markexpr", default="", metavar="MARKEXPR", - help="only run tests matching given mark expression. " - "example: -m 'mark1 and not mark2'.", + help="only run tests matching given mark expression.\n" + "For example: -m 'mark1 and not mark2'.", ) group.addoption( From 35d136161a577cc5ca75446d90887686ebad966c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 25 Feb 2020 16:05:17 +0100 Subject: [PATCH 228/823] add test Fixes the test to not match e.g. hypothesis (ref: bdde2ac28). Conflicts: testing/test_helpconfig.py --- testing/test_helpconfig.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index aaf9a2e2807..5e4f852280d 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -19,7 +19,10 @@ def test_help(testdir): assert result.ret == 0 result.stdout.fnmatch_lines( """ - *-v*verbose* + -m MARKEXPR only run tests matching given mark expression. + For example: -m 'mark1 and not mark2'. + reporting: + --durations=N * *setup.cfg* *minversion* *to see*markers*pytest --markers* From 5a2c69f150d0b537fbb2ac89ad427c34bd60f31b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 19 May 2020 19:33:51 -0300 Subject: [PATCH 229/823] Add CHANGELOG for #6817 --- changelog/6817.improvement.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog/6817.improvement.rst diff --git a/changelog/6817.improvement.rst b/changelog/6817.improvement.rst new file mode 100644 index 00000000000..8d7e30d3467 --- /dev/null +++ b/changelog/6817.improvement.rst @@ -0,0 +1,2 @@ +Explicit new-lines in help texts of command-line options are preserved, allowing plugins better control +of the help displayed to users. From b337a9a66d64618692ac12773867168ab35995a2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 8 May 2020 18:25:40 +0300 Subject: [PATCH 230/823] CONTRIBUTING: add section about backporting fixes to patch releases --- CONTRIBUTING.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ea424f1e6e7..3d07db63804 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -329,6 +329,36 @@ should go into ``test_cacheprovider.py``, given that this option is implemented If in doubt, go ahead and open a PR with your best guess and we can discuss this over the code. +Backporting bug fixes for the next patch release +------------------------------------------------ + +Pytest makes feature release every few weeks or months. In between, patch releases +are made to the previous feature release, containing bug fixes only. The bug fixes +usually fix regressions, but may be any change that should reach users before the +next feature release. + +Suppose for example that the latest release was 1.2.3, and you want to include +a bug fix in 1.2.4 (check https://github.com/pytest-dev/pytest/releases for the +actual latest release). The procedure for this is: + +#. First, make sure the bug is fixed the ``master`` branch, with a regular pull + request, as described above. An exception to this is if the bug fix is not + applicable to ``master`` anymore. + +#. ``git checkout origin/1.2.x -b backport-XXXX`` # use the master PR number here + +#. Locate the merge commit on the PR, in the *merged* message, for example: + + nicoddemus merged commit 0f8b462 into pytest-dev:master + +#. ``git cherry-pick -x -m1 REVISION`` # use the revision you found above (``0f8b462``). + +#. Open a PR targeting ``1.2.x``: + + * Prefix the message with ``[1.2.x]``. + * Delete the PR body, it usually contains a duplicate commit message. + + Joining the Development Team ---------------------------- From eaeafd7c3050da48ca71be0c1781ff36f60ca4f7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 15 Mar 2020 23:32:59 +0200 Subject: [PATCH 231/823] Perform FD capturing even if the FD is invalid The `FDCapture`/`FDCaptureBinary` classes, used by `capfd`/`capfdbinary` fixtures and the `--capture=fd` option (set by default), redirect FDs 1/2 (stdout/stderr) to a temporary file. To do this, they need to save the old file by duplicating the FD before redirecting it, to be restored once finished. Previously, if this duplicating (`os.dup()`) failed, most likely due to that FD being invalid, the FD redirection would silently not be done. The FD capturing also performs python-level redirection (monkeypatching `sys.stdout`/`sys.stderr`) which would still be done, but direct writes to the FDs would fail. This is not great. If pytest is run with `--capture=fd`, or a test is using `capfd`, it expects writes to the FD to work and be captured, regardless of external circumstances. So, instead of disabling FD capturing, keep the redirection to a temporary file, just don't restore it after closing, because there is nothing to restore to. --- changelog/7091.improvement.rst | 4 ++ src/_pytest/capture.py | 86 +++++++++++++++++++--------------- testing/test_capture.py | 48 +++++++++++++++++-- 3 files changed, 95 insertions(+), 43 deletions(-) create mode 100644 changelog/7091.improvement.rst diff --git a/changelog/7091.improvement.rst b/changelog/7091.improvement.rst new file mode 100644 index 00000000000..72f17c5e48e --- /dev/null +++ b/changelog/7091.improvement.rst @@ -0,0 +1,4 @@ +When ``fd`` capturing is used, through ``--capture=fd`` or the ``capfd`` and +``capfdbinary`` fixtures, and the file descriptor (0, 1, 2) cannot be +duplicated, FD capturing is still performed. Previously, direct writes to the +file descriptors would fail or be lost in this case. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 7eafeb3e406..323881151d9 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -513,49 +513,57 @@ class FDCaptureBinary: def __init__(self, targetfd, tmpfile=None): self.targetfd = targetfd + try: - self.targetfd_save = os.dup(self.targetfd) + os.fstat(targetfd) except OSError: - self.start = lambda: None - self.done = lambda: None + # FD capturing is conceptually simple -- create a temporary file, + # redirect the FD to it, redirect back when done. But when the + # target FD is invalid it throws a wrench into this loveley scheme. + # + # Tests themselves shouldn't care if the FD is valid, FD capturing + # should work regardless of external circumstances. So falling back + # to just sys capturing is not a good option. + # + # Further complications are the need to support suspend() and the + # possibility of FD reuse (e.g. the tmpfile getting the very same + # target FD). The following approach is robust, I believe. + self.targetfd_invalid = os.open(os.devnull, os.O_RDWR) + os.dup2(self.targetfd_invalid, targetfd) + else: + self.targetfd_invalid = None + self.targetfd_save = os.dup(targetfd) + + if targetfd == 0: + assert not tmpfile, "cannot set tmpfile with stdin" + tmpfile = open(os.devnull) + self.syscapture = SysCapture(targetfd) else: - self.start = self._start - self.done = self._done - if targetfd == 0: - assert not tmpfile, "cannot set tmpfile with stdin" - tmpfile = open(os.devnull) - self.syscapture = SysCapture(targetfd) + if tmpfile is None: + tmpfile = EncodedFile( + TemporaryFile(buffering=0), + encoding="utf-8", + errors="replace", + write_through=True, + ) + if targetfd in patchsysdict: + self.syscapture = SysCapture(targetfd, tmpfile) else: - if tmpfile is None: - tmpfile = EncodedFile( - TemporaryFile(buffering=0), - encoding="utf-8", - errors="replace", - write_through=True, - ) - if targetfd in patchsysdict: - self.syscapture = SysCapture(targetfd, tmpfile) - else: - self.syscapture = NoCapture() - self.tmpfile = tmpfile - self.tmpfile_fd = tmpfile.fileno() + self.syscapture = NoCapture() + self.tmpfile = tmpfile def __repr__(self): - return "<{} {} oldfd={} _state={!r} tmpfile={}>".format( + return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format( self.__class__.__name__, self.targetfd, - getattr(self, "targetfd_save", ""), + self.targetfd_save, self._state, - hasattr(self, "tmpfile") and repr(self.tmpfile) or "", + self.tmpfile, ) - def _start(self): + def start(self): """ Start capturing on targetfd using memorized tmpfile. """ - try: - os.fstat(self.targetfd_save) - except (AttributeError, OSError): - raise ValueError("saved filedescriptor not valid anymore") - os.dup2(self.tmpfile_fd, self.targetfd) + os.dup2(self.tmpfile.fileno(), self.targetfd) self.syscapture.start() self._state = "started" @@ -566,12 +574,15 @@ def snap(self): self.tmpfile.truncate() return res - def _done(self): + def done(self): """ stop capturing, restore streams, return original capture file, seeked to position zero. """ - targetfd_save = self.__dict__.pop("targetfd_save") - os.dup2(targetfd_save, self.targetfd) - os.close(targetfd_save) + os.dup2(self.targetfd_save, self.targetfd) + os.close(self.targetfd_save) + if self.targetfd_invalid is not None: + if self.targetfd_invalid != self.targetfd: + os.close(self.targetfd) + os.close(self.targetfd_invalid) self.syscapture.done() self.tmpfile.close() self._state = "done" @@ -583,7 +594,7 @@ def suspend(self): def resume(self): self.syscapture.resume() - os.dup2(self.tmpfile_fd, self.targetfd) + os.dup2(self.tmpfile.fileno(), self.targetfd) self._state = "resumed" def writeorg(self, data): @@ -609,8 +620,7 @@ def snap(self): def writeorg(self, data): """ write to original file descriptor. """ - data = data.encode("utf-8") # XXX use encoding of original stream - os.write(self.targetfd_save, data) + super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream class SysCaptureBinary: diff --git a/testing/test_capture.py b/testing/test_capture.py index c064614d238..177d72ebc3d 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -943,8 +943,8 @@ def test_simple_resume_suspend(self): pytest.raises(AttributeError, cap.suspend) assert repr(cap) == ( - " _state='done' tmpfile={!r}>".format( - cap.tmpfile + "".format( + cap.targetfd_save, cap.tmpfile ) ) # Should not crash with missing "_old". @@ -1150,6 +1150,7 @@ def test_stdcapture_fd_invalid_fd(self, testdir): testdir.makepyfile( """ import os + from fnmatch import fnmatch from _pytest import capture def StdCaptureFD(out=True, err=True, in_=True): @@ -1158,19 +1159,25 @@ def StdCaptureFD(out=True, err=True, in_=True): def test_stdout(): os.close(1) cap = StdCaptureFD(out=True, err=False, in_=False) - assert repr(cap.out) == " _state=None tmpfile=>" + assert fnmatch(repr(cap.out), "") + cap.start_capturing() + os.write(1, b"stdout") + assert cap.readouterr() == ("stdout", "") cap.stop_capturing() def test_stderr(): os.close(2) cap = StdCaptureFD(out=False, err=True, in_=False) - assert repr(cap.err) == " _state=None tmpfile=>" + assert fnmatch(repr(cap.err), "") + cap.start_capturing() + os.write(2, b"stderr") + assert cap.readouterr() == ("", "stderr") cap.stop_capturing() def test_stdin(): os.close(0) cap = StdCaptureFD(out=False, err=False, in_=True) - assert repr(cap.in_) == " _state=None tmpfile=>" + assert fnmatch(repr(cap.in_), "") cap.stop_capturing() """ ) @@ -1178,6 +1185,37 @@ def test_stdin(): assert result.ret == 0 assert result.parseoutcomes()["passed"] == 3 + def test_fdcapture_invalid_fd_with_fd_reuse(self, testdir): + with saved_fd(1): + os.close(1) + cap = capture.FDCaptureBinary(1) + cap.start() + os.write(1, b"started") + cap.suspend() + os.write(1, b" suspended") + cap.resume() + os.write(1, b" resumed") + assert cap.snap() == b"started resumed" + cap.done() + with pytest.raises(OSError): + os.write(1, b"done") + + def test_fdcapture_invalid_fd_without_fd_reuse(self, testdir): + with saved_fd(1), saved_fd(2): + os.close(1) + os.close(2) + cap = capture.FDCaptureBinary(2) + cap.start() + os.write(2, b"started") + cap.suspend() + os.write(2, b" suspended") + cap.resume() + os.write(2, b" resumed") + assert cap.snap() == b"started resumed" + cap.done() + with pytest.raises(OSError): + os.write(2, b"done") + def test_capture_not_started_but_reset(): capsys = StdCapture() From fa6a13a7cc4460987d04088726104877a029b259 Mon Sep 17 00:00:00 2001 From: mcsitter <48606431+mcsitter@users.noreply.github.com> Date: Wed, 20 May 2020 22:41:52 +0200 Subject: [PATCH 232/823] Updated compatible python versions As indicated on pypi and in the README pytest supports python version 3.8 and 3.9. The documentation should match. --- AUTHORS | 1 + doc/en/getting-started.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index dd99adff583..5822c74f2cf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -187,6 +187,7 @@ Matt Duck Matt Williams Matthias Hafner Maxim Filipenko +Maximilian Cosmo Sitter mbyt Michael Aquilina Michael Birtwell diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 2549e8ff62d..56057434edc 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -1,7 +1,7 @@ Installation and Getting Started =================================== -**Pythons**: Python 3.5, 3.6, 3.7, PyPy3 +**Pythons**: Python 3.5, 3.6, 3.7, 3.8, 3.9, PyPy3 **Platforms**: Linux and Windows From 8d841ab0b89e08777390577584e081780e1ad32d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 19 May 2020 20:52:04 +0300 Subject: [PATCH 233/823] nodes: remove unused argument from FSHookProxy --- src/_pytest/nodes.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 448e6712797..2f5f9bdb801 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -456,10 +456,7 @@ def _check_initialpaths_for_relpath(session, fspath): class FSHookProxy: - def __init__( - self, fspath: py.path.local, pm: PytestPluginManager, remove_mods - ) -> None: - self.fspath = fspath + def __init__(self, pm: PytestPluginManager, remove_mods) -> None: self.pm = pm self.remove_mods = remove_mods @@ -510,7 +507,7 @@ def _gethookproxy(self, fspath: py.path.local): remove_mods = pm._conftest_plugins.difference(my_conftestmodules) if remove_mods: # one or more conftests are not in use at this fspath - proxy = FSHookProxy(fspath, pm, remove_mods) + proxy = FSHookProxy(pm, remove_mods) else: # all plugins are active for this fspath proxy = self.config.hook From 139a029b5e885fd3756c2fe0013035a4e84c4fe9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 19 May 2020 21:34:06 +0300 Subject: [PATCH 234/823] terminal: remove a redundant line `write_fspath_result` already does this split. --- src/_pytest/terminal.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 3de0612bf4b..a8122aafdaf 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -443,8 +443,7 @@ def pytest_runtest_logstart(self, nodeid, location): self.write_ensure_prefix(line, "") self.flush() elif self.showfspath: - fsid = nodeid.split("::")[0] - self.write_fspath_result(fsid, "") + self.write_fspath_result(nodeid, "") self.flush() def pytest_runtest_logreport(self, report: TestReport) -> None: From 796fba67880c9ce2011d4183dd574339e26618fa Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 19 May 2020 21:41:28 +0300 Subject: [PATCH 235/823] terminal: remove redundant write_fspath_result call This is already done in pytest_runtest_logstart, so the fspath is already guaranteed to have been printed (for xdist, it is disabled anyway). write_fspath_result is mildly expensive so it is worth avoiding calling it twice. --- src/_pytest/terminal.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index a8122aafdaf..8ecb5a16b63 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -473,10 +473,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: else: markup = {} if self.verbosity <= 0: - if not running_xdist and self.showfspath: - self.write_fspath_result(rep.nodeid, letter, **markup) - else: - self._tw.write(letter, **markup) + self._tw.write(letter, **markup) else: self._progress_nodeids_reported.add(rep.nodeid) line = self._locationline(rep.nodeid, *rep.location) From f1f9c7792bd66413dad208d461975571789d4f10 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 13 May 2020 16:59:44 +0300 Subject: [PATCH 236/823] Import `packaging` package lazily --- src/_pytest/config/__init__.py | 4 +++- src/_pytest/outcomes.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 68c3822d05e..bb5034ab1f2 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -23,7 +23,6 @@ import attr import py -from packaging.version import Version from pluggy import HookimplMarker from pluggy import HookspecMarker from pluggy import PluginManager @@ -1059,6 +1058,9 @@ def _checkversion(self): minver = self.inicfg.get("minversion", None) if minver: + # Imported lazily to improve start-up time. + from packaging.version import Version + if Version(minver) > Version(pytest.__version__): raise pytest.UsageError( "%s:%d: requires pytest-%s, actual pytest-%s'" diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 7d7e9df7af2..751cf9474fb 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -9,8 +9,6 @@ from typing import Optional from typing import TypeVar -from packaging.version import Version - TYPE_CHECKING = False # avoid circular import through compat if TYPE_CHECKING: @@ -217,6 +215,9 @@ def importorskip( return mod verattr = getattr(mod, "__version__", None) if minversion is not None: + # Imported lazily to improve start-up time. + from packaging.version import Version + if verattr is None or Version(verattr) < Version(minversion): raise Skipped( "module %r has __version__ %r, required is: %r" From 62d3577435ccd532b9ad41c197e6149eafd713f0 Mon Sep 17 00:00:00 2001 From: Florian Dahlitz Date: Fri, 22 May 2020 16:33:50 +0200 Subject: [PATCH 237/823] Add note about --strict and --strict-markers to references --- AUTHORS | 1 + changelog/7233.doc.rst | 1 + doc/en/reference.rst | 5 +++++ 3 files changed, 7 insertions(+) create mode 100644 changelog/7233.doc.rst diff --git a/AUTHORS b/AUTHORS index 5822c74f2cf..f7b87fd466c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -102,6 +102,7 @@ Fabio Zadrozny Felix Nieuwenhuizen Feng Ma Florian Bruhin +Florian Dahlitz Floris Bruynooghe Gabriel Reis Gene Wood diff --git a/changelog/7233.doc.rst b/changelog/7233.doc.rst new file mode 100644 index 00000000000..c57f4d61f12 --- /dev/null +++ b/changelog/7233.doc.rst @@ -0,0 +1 @@ +Add a note about ``--strict`` and ``--strict-markers`` and the preference for the latter one. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 0059b4cb278..6bc7657c570 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1447,6 +1447,11 @@ passed multiple times. The expected format is ``name=value``. For example:: slow serial + .. note:: + The use of ``--strict-markers`` is highly preferred. ``--strict`` was kept for + backward compatibility only and may be confusing for others as it only applies to + markers and not to other options. + .. confval:: minversion Specifies a minimal pytest version required for running tests. From d0eb86cfa671057c678a90ac10c1a43be11bdf6a Mon Sep 17 00:00:00 2001 From: Florian Dahlitz Date: Fri, 22 May 2020 22:58:35 +0200 Subject: [PATCH 238/823] Prevent hiding underlying exception when ConfTestImportFailure is raised --- changelog/7150.bugfix.rst | 1 + src/_pytest/debugging.py | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 changelog/7150.bugfix.rst diff --git a/changelog/7150.bugfix.rst b/changelog/7150.bugfix.rst new file mode 100644 index 00000000000..42cf5c7d2b9 --- /dev/null +++ b/changelog/7150.bugfix.rst @@ -0,0 +1 @@ +Prevent hiding the underlying exception when ``ConfTestImportFailure`` is raised. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 17915db73fc..26c3095dccd 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -4,6 +4,7 @@ import sys from _pytest import outcomes +from _pytest.config import ConftestImportFailure from _pytest.config import hookimpl from _pytest.config.exceptions import UsageError @@ -338,6 +339,10 @@ def _postmortem_traceback(excinfo): # A doctest.UnexpectedException is not useful for post_mortem. # Use the underlying exception instead: return excinfo.value.exc_info[2] + elif isinstance(excinfo.value, ConftestImportFailure): + # A config.ConftestImportFailure is not useful for post_mortem. + # Use the underlying exception instead: + return excinfo.value.excinfo[2] else: return excinfo._excinfo[2] From 05c22ff82363c1d11fa6d593dd4f404819abc69e Mon Sep 17 00:00:00 2001 From: Simon K Date: Sat, 23 May 2020 15:27:06 +0100 Subject: [PATCH 239/823] 7154-Improve-testdir-documentation-on-makefiles (#7239) --- src/_pytest/pytester.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 32b03bd4af6..1da4787361c 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -687,11 +687,41 @@ def getinicfg(self, source): return py.iniconfig.IniConfig(p)["pytest"] def makepyfile(self, *args, **kwargs): - """Shortcut for .makefile() with a .py extension.""" + r"""Shortcut for .makefile() with a .py extension. + Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting + existing files. + + Examples: + + .. code-block:: python + + def test_something(testdir): + # initial file is created test_something.py + testdir.makepyfile("foobar") + # to create multiple files, pass kwargs accordingly + testdir.makepyfile(custom="foobar") + # at this point, both 'test_something.py' & 'custom.py' exist in the test directory + + """ return self._makefile(".py", args, kwargs) def maketxtfile(self, *args, **kwargs): - """Shortcut for .makefile() with a .txt extension.""" + r"""Shortcut for .makefile() with a .txt extension. + Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting + existing files. + + Examples: + + .. code-block:: python + + def test_something(testdir): + # initial file is created test_something.txt + testdir.maketxtfile("foobar") + # to create multiple files, pass kwargs accordingly + testdir.maketxtfile(custom="foobar") + # at this point, both 'test_something.txt' & 'custom.txt' exist in the test directory + + """ return self._makefile(".txt", args, kwargs) def syspathinsert(self, path=None): From 79701c65ed47d9fd9dbc8949a49136f25506d9e0 Mon Sep 17 00:00:00 2001 From: Claire Cecil Date: Sat, 23 May 2020 10:27:58 -0400 Subject: [PATCH 240/823] Added support for less verbose version information (#7169) --- AUTHORS | 1 + changelog/7128.improvement.rst | 1 + src/_pytest/helpconfig.py | 28 +++++++++++++++++----------- testing/test_config.py | 4 +--- testing/test_helpconfig.py | 13 ++++++++++--- 5 files changed, 30 insertions(+), 17 deletions(-) create mode 100644 changelog/7128.improvement.rst diff --git a/AUTHORS b/AUTHORS index 5822c74f2cf..64907c1d820 100644 --- a/AUTHORS +++ b/AUTHORS @@ -63,6 +63,7 @@ Christian Tismer Christoph Buelter Christopher Dignam Christopher Gilling +Claire Cecil Claudio Madotto CrazyMerlyn Cyrus Maden diff --git a/changelog/7128.improvement.rst b/changelog/7128.improvement.rst new file mode 100644 index 00000000000..9d24d567aed --- /dev/null +++ b/changelog/7128.improvement.rst @@ -0,0 +1 @@ +`pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 11fd024628c..402ffae66cf 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -41,8 +41,11 @@ def pytest_addoption(parser): group.addoption( "--version", "-V", - action="store_true", - help="display pytest version and information about plugins.", + action="count", + default=0, + dest="version", + help="display pytest version and information about plugins." + "When given twice, also display information about plugins.", ) group._addoption( "-h", @@ -116,19 +119,22 @@ def unset_tracing(): def showversion(config): - sys.stderr.write( - "This is pytest version {}, imported from {}\n".format( - pytest.__version__, pytest.__file__ + if config.option.version > 1: + sys.stderr.write( + "This is pytest version {}, imported from {}\n".format( + pytest.__version__, pytest.__file__ + ) ) - ) - plugininfo = getpluginversioninfo(config) - if plugininfo: - for line in plugininfo: - sys.stderr.write(line + "\n") + plugininfo = getpluginversioninfo(config) + if plugininfo: + for line in plugininfo: + sys.stderr.write(line + "\n") + else: + sys.stderr.write("pytest {}\n".format(pytest.__version__)) def pytest_cmdline_main(config): - if config.option.version: + if config.option.version > 0: showversion(config) return 0 elif config.option.help: diff --git a/testing/test_config.py b/testing/test_config.py index 7d553e63b7c..17385dc17f5 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1243,9 +1243,7 @@ def pytest_addoption(parser): assert result.ret == ExitCode.USAGE_ERROR result = testdir.runpytest("--version") - result.stderr.fnmatch_lines( - ["*pytest*{}*imported from*".format(pytest.__version__)] - ) + result.stderr.fnmatch_lines(["pytest {}".format(pytest.__version__)]) assert result.ret == ExitCode.USAGE_ERROR diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index 5e4f852280d..24590dd3b55 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -2,11 +2,10 @@ from _pytest.config import ExitCode -def test_version(testdir, pytestconfig): +def test_version_verbose(testdir, pytestconfig): testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") - result = testdir.runpytest("--version") + result = testdir.runpytest("--version", "--version") assert result.ret == 0 - # p = py.path.local(py.__file__).dirpath() result.stderr.fnmatch_lines( ["*pytest*{}*imported from*".format(pytest.__version__)] ) @@ -14,6 +13,14 @@ def test_version(testdir, pytestconfig): result.stderr.fnmatch_lines(["*setuptools registered plugins:", "*at*"]) +def test_version_less_verbose(testdir, pytestconfig): + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + result = testdir.runpytest("--version") + assert result.ret == 0 + # p = py.path.local(py.__file__).dirpath() + result.stderr.fnmatch_lines(["pytest {}".format(pytest.__version__)]) + + def test_help(testdir): result = testdir.runpytest("--help") assert result.ret == 0 From 1780924b279a80aa3cccd840229dfe3e6f60e88b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 22 May 2020 16:10:51 -0300 Subject: [PATCH 241/823] Introduce _pytest.timing as a way to control timing during tests _pytest.timing is an indirection to 'time' functions, which pytest production code should use instead of 'time' directly. 'mock_timing' is a new fixture which then mocks those functions, allowing us to write time-reliable tests which run instantly and are not flaky. This was triggered by recent flaky junitxml tests on Windows related to timing issues. --- src/_pytest/junitxml.py | 8 ++--- src/_pytest/pytester.py | 10 +++---- src/_pytest/runner.py | 11 ++++--- src/_pytest/terminal.py | 10 +++---- src/_pytest/timing.py | 13 +++++++++ testing/acceptance_test.py | 60 ++++++++++++-------------------------- testing/conftest.py | 38 ++++++++++++++++++++++++ testing/test_junitxml.py | 12 ++++---- 8 files changed, 94 insertions(+), 68 deletions(-) create mode 100644 src/_pytest/timing.py diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 77e1843127a..4f9831e064e 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -13,7 +13,6 @@ import platform import re import sys -import time from datetime import datetime import py @@ -21,6 +20,7 @@ import pytest from _pytest import deprecated from _pytest import nodes +from _pytest import timing from _pytest.config import filename_arg from _pytest.store import StoreKey from _pytest.warnings import _issue_warning_captured @@ -627,14 +627,14 @@ def pytest_internalerror(self, excrepr): reporter._add_simple(Junit.error, "internal error", excrepr) def pytest_sessionstart(self): - self.suite_start_time = time.time() + self.suite_start_time = timing.time() def pytest_sessionfinish(self): dirname = os.path.dirname(os.path.abspath(self.logfile)) if not os.path.isdir(dirname): os.makedirs(dirname) logfile = open(self.logfile, "w", encoding="utf-8") - suite_stop_time = time.time() + suite_stop_time = timing.time() suite_time_delta = suite_stop_time - self.suite_start_time numtests = ( @@ -662,7 +662,7 @@ def pytest_sessionfinish(self): logfile.close() def pytest_terminal_summary(self, terminalreporter): - terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile)) + terminalreporter.write_sep("-", "generated xml file: {}".format(self.logfile)) def add_global_property(self, name, value): __tracebackhide__ = True diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 32b03bd4af6..07250b2458d 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -7,7 +7,6 @@ import re import subprocess import sys -import time import traceback from fnmatch import fnmatch from io import StringIO @@ -24,6 +23,7 @@ import py import pytest +from _pytest import timing from _pytest._code import Source from _pytest.capture import MultiCapture from _pytest.capture import SysCapture @@ -941,7 +941,7 @@ def runpytest_inprocess(self, *args, **kwargs) -> RunResult: if syspathinsert: self.syspathinsert() - now = time.time() + now = timing.time() capture = MultiCapture(Capture=SysCapture) capture.start_capturing() try: @@ -970,7 +970,7 @@ class reprec: # type: ignore sys.stderr.write(err) res = RunResult( - reprec.ret, out.splitlines(), err.splitlines(), time.time() - now + reprec.ret, out.splitlines(), err.splitlines(), timing.time() - now ) res.reprec = reprec # type: ignore return res @@ -1171,7 +1171,7 @@ def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: f1 = open(str(p1), "w", encoding="utf8") f2 = open(str(p2), "w", encoding="utf8") try: - now = time.time() + now = timing.time() popen = self.popen( cmdargs, stdin=stdin, @@ -1218,7 +1218,7 @@ def handle_timeout(): ret = ExitCode(ret) except ValueError: pass - return RunResult(ret, out, err, time.time() - now) + return RunResult(ret, out, err, timing.time() - now) def _dump_lines(self, lines, fp): try: diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index e7211369cc8..aa8a5aa8b42 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -2,8 +2,6 @@ import bdb import os import sys -from time import perf_counter # Intentionally not `import time` to avoid being -from time import time # affected by tests which monkeypatch `time` (issue #185). from typing import Callable from typing import Dict from typing import List @@ -15,6 +13,7 @@ from .reports import CollectErrorRepr from .reports import CollectReport from .reports import TestReport +from _pytest import timing from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest.compat import TYPE_CHECKING @@ -254,8 +253,8 @@ def from_call(cls, func, when, reraise=None) -> "CallInfo": #: context of invocation: one of "setup", "call", #: "teardown", "memocollect" excinfo = None - start = time() - precise_start = perf_counter() + start = timing.time() + precise_start = timing.perf_counter() try: result = func() except BaseException: @@ -264,9 +263,9 @@ def from_call(cls, func, when, reraise=None) -> "CallInfo": raise result = None # use the perf counter - precise_stop = perf_counter() + precise_stop = timing.perf_counter() duration = precise_stop - precise_start - stop = time() + stop = timing.time() return cls( start=start, stop=stop, diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 3de0612bf4b..c0962162870 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -7,7 +7,6 @@ import inspect import platform import sys -import time import warnings from functools import partial from typing import Any @@ -26,6 +25,7 @@ import pytest from _pytest import nodes +from _pytest import timing from _pytest._io import TerminalWriter from _pytest.compat import order_preserving_dict from _pytest.config import Config @@ -557,7 +557,7 @@ def pytest_collection(self) -> None: if self.isatty: if self.config.option.verbose >= 0: self.write("collecting ... ", flush=True, bold=True) - self._collect_report_last_write = time.time() + self._collect_report_last_write = timing.time() elif self.config.option.verbose >= 1: self.write("collecting ... ", flush=True, bold=True) @@ -577,7 +577,7 @@ def report_collect(self, final=False): if not final: # Only write "collecting" report every 0.5s. - t = time.time() + t = timing.time() if ( self._collect_report_last_write is not None and self._collect_report_last_write > t - REPORT_COLLECTING_RESOLUTION @@ -614,7 +614,7 @@ def report_collect(self, final=False): @pytest.hookimpl(trylast=True) def pytest_sessionstart(self, session: Session) -> None: self._session = session - self._sessionstarttime = time.time() + self._sessionstarttime = timing.time() if not self.showheader: return self.write_sep("=", "test session starts", bold=True) @@ -969,7 +969,7 @@ def summary_stats(self): if self.verbosity < -1: return - session_duration = time.time() - self._sessionstarttime + session_duration = timing.time() - self._sessionstarttime (parts, main_color) = self.build_summary_stats_line() line_parts = [] diff --git a/src/_pytest/timing.py b/src/_pytest/timing.py new file mode 100644 index 00000000000..ded917b35bd --- /dev/null +++ b/src/_pytest/timing.py @@ -0,0 +1,13 @@ +""" +Indirection for time functions. + +We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect +pytest runtime information (issue #185). + +Fixture "mock_timinig" also interacts with this module for pytest's own tests. +""" +from time import perf_counter +from time import sleep +from time import time + +__all__ = ["perf_counter", "sleep", "time"] diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 45a23ee935d..e2df92d8091 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -6,11 +6,9 @@ import attr import py -import _pytest.runner import pytest from _pytest.compat import importlib_metadata from _pytest.config import ExitCode -from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Testdir @@ -896,37 +894,21 @@ def test_has_plugin(self, request): assert request.config.pluginmanager.hasplugin("python") -def fake_time(monkeypatch: MonkeyPatch) -> None: - """Monkeypatch time functions to make TestDurations not rely on actual time.""" - import time - - current_time = 1586202699.9859412 - - def sleep(seconds: float) -> None: - nonlocal current_time - current_time += seconds - - monkeypatch.setattr(time, "sleep", sleep) - monkeypatch.setattr(_pytest.runner, "time", lambda: current_time) - monkeypatch.setattr(_pytest.runner, "perf_counter", lambda: current_time) - - class TestDurations: source = """ - import time + from _pytest import timing def test_something(): pass def test_2(): - time.sleep(0.010) + timing.sleep(0.010) def test_1(): - time.sleep(0.002) + timing.sleep(0.002) def test_3(): - time.sleep(0.020) + timing.sleep(0.020) """ - def test_calls(self, testdir): + def test_calls(self, testdir, mock_timing): testdir.makepyfile(self.source) - fake_time(testdir.monkeypatch) result = testdir.runpytest_inprocess("--durations=10") assert result.ret == 0 @@ -938,18 +920,17 @@ def test_calls(self, testdir): ["(8 durations < 0.005s hidden. Use -vv to show these durations.)"] ) - def test_calls_show_2(self, testdir): + def test_calls_show_2(self, testdir, mock_timing): + testdir.makepyfile(self.source) - fake_time(testdir.monkeypatch) result = testdir.runpytest_inprocess("--durations=2") assert result.ret == 0 lines = result.stdout.get_lines_after("*slowest*durations*") assert "4 passed" in lines[2] - def test_calls_showall(self, testdir): + def test_calls_showall(self, testdir, mock_timing): testdir.makepyfile(self.source) - fake_time(testdir.monkeypatch) result = testdir.runpytest_inprocess("--durations=0") assert result.ret == 0 @@ -962,9 +943,8 @@ def test_calls_showall(self, testdir): else: raise AssertionError("not found {} {}".format(x, y)) - def test_calls_showall_verbose(self, testdir): + def test_calls_showall_verbose(self, testdir, mock_timing): testdir.makepyfile(self.source) - fake_time(testdir.monkeypatch) result = testdir.runpytest_inprocess("--durations=0", "-vv") assert result.ret == 0 @@ -976,17 +956,15 @@ def test_calls_showall_verbose(self, testdir): else: raise AssertionError("not found {} {}".format(x, y)) - def test_with_deselected(self, testdir): + def test_with_deselected(self, testdir, mock_timing): testdir.makepyfile(self.source) - fake_time(testdir.monkeypatch) result = testdir.runpytest_inprocess("--durations=2", "-k test_3") assert result.ret == 0 result.stdout.fnmatch_lines(["*durations*", "*call*test_3*"]) - def test_with_failing_collection(self, testdir): + def test_with_failing_collection(self, testdir, mock_timing): testdir.makepyfile(self.source) - fake_time(testdir.monkeypatch) testdir.makepyfile(test_collecterror="""xyz""") result = testdir.runpytest_inprocess("--durations=2", "-k test_1") assert result.ret == 2 @@ -996,9 +974,8 @@ def test_with_failing_collection(self, testdir): # output result.stdout.no_fnmatch_line("*duration*") - def test_with_not(self, testdir): + def test_with_not(self, testdir, mock_timing): testdir.makepyfile(self.source) - fake_time(testdir.monkeypatch) result = testdir.runpytest_inprocess("-k not 1") assert result.ret == 0 @@ -1006,27 +983,26 @@ def test_with_not(self, testdir): class TestDurationsWithFixture: source = """ import pytest - import time + from _pytest import timing @pytest.fixture def setup_fixt(): - time.sleep(0.02) + timing.sleep(2) def test_1(setup_fixt): - time.sleep(0.02) + timing.sleep(5) """ - def test_setup_function(self, testdir): + def test_setup_function(self, testdir, mock_timing): testdir.makepyfile(self.source) - fake_time(testdir.monkeypatch) result = testdir.runpytest_inprocess("--durations=10") assert result.ret == 0 result.stdout.fnmatch_lines_random( """ *durations* - * setup *test_1* - * call *test_1* + 5.00s call *test_1* + 2.00s setup *test_1* """ ) diff --git a/testing/conftest.py b/testing/conftest.py index 58386b162bb..f430189489e 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -197,3 +197,41 @@ def requires_ordered_markup(cls, result: RunResult): ) return ColorMapping + + +@pytest.fixture +def mock_timing(monkeypatch): + """Mocks _pytest.timing with a known object that can be used to control timing in tests + deterministically. + + pytest itself should always use functions from `_pytest.timing` instead of `time` directly. + + This then allows us more control over time during testing, if testing code also + uses `_pytest.timing` functions. + + Time is static, and only advances through `sleep` calls, thus tests might sleep over large + numbers and obtain accurate time() calls at the end, making tests reliable and instant. + """ + import attr + + @attr.s + class MockTiming: + + _current_time = attr.ib(default=1590150050.0) + + def sleep(self, seconds): + self._current_time += seconds + + def time(self): + return self._current_time + + def patch(self): + from _pytest import timing + + monkeypatch.setattr(timing, "sleep", self.sleep) + monkeypatch.setattr(timing, "time", self.time) + monkeypatch.setattr(timing, "perf_counter", self.time) + + result = MockTiming() + result.patch() + return result diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index a1f86b0b85f..83e61e1d9de 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -201,23 +201,23 @@ def test_pass(): timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f") assert start_time <= timestamp < datetime.now() - def test_timing_function(self, testdir, run_and_parse): + def test_timing_function(self, testdir, run_and_parse, mock_timing): testdir.makepyfile( """ - import time, pytest + from _pytest import timing def setup_module(): - time.sleep(0.01) + timing.sleep(1) def teardown_module(): - time.sleep(0.01) + timing.sleep(2) def test_sleep(): - time.sleep(0.01) + timing.sleep(4) """ ) result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") val = tnode["time"] - assert round(float(val), 2) >= 0.03 + assert float(val) == 7.0 @pytest.mark.parametrize("duration_report", ["call", "total"]) def test_junit_duration_report( From 35e6dd01173de0b58e175d79e58d6295c359a0bd Mon Sep 17 00:00:00 2001 From: Florian Dahlitz Date: Sat, 23 May 2020 18:19:33 +0200 Subject: [PATCH 242/823] Add test for exposure of underlying exception --- testing/test_debugging.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 719d6477bff..00af4a088a7 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -342,6 +342,15 @@ def pytest_runtest_protocol(): child.sendeof() self.flush(child) + def test_pdb_prevent_ConftestImportFailure_hiding_exception(self, testdir): + testdir.makepyfile("def test_func(): pass") + sub_dir = testdir.tmpdir.join("ns").ensure_dir() + sub_dir.join("conftest").new(ext=".py").write("import unknown") + sub_dir.join("test_file").new(ext=".py").write("def test_func(): pass") + + result = testdir.runpytest_subprocess("--pdb", ".") + result.stdout.fnmatch_lines(["-> import unknown"]) + def test_pdb_interaction_capturing_simple(self, testdir): p1 = testdir.makepyfile( """ From c9abdaf3811600bfd552151102cf19b5798410d1 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 23 May 2020 13:29:36 -0700 Subject: [PATCH 243/823] Use deadsnakes/action@v1.0.0 to install python3.9 nightly --- .github/workflows/main.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 45e386e5702..6d1910014ed 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -94,7 +94,7 @@ jobs: os: ubuntu-latest tox_env: "py38-xdist" - name: "ubuntu-py39" - python: "3.8" + python: "3.9-dev" os: ubuntu-latest tox_env: "py39-xdist" - name: "ubuntu-pypy3" @@ -132,14 +132,14 @@ jobs: - run: git fetch --prune --unshallow - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v2 + if: matrix.python != '3.9-dev' + with: + python-version: ${{ matrix.python }} + - name: Set up Python ${{ matrix.python }} (deadsnakes) + uses: deadsnakes/action@v1.0.0 + if: matrix.python == '3.9-dev' with: python-version: ${{ matrix.python }} - - name: install python3.9 - if: matrix.tox_env == 'py39-xdist' - run: | - sudo add-apt-repository ppa:deadsnakes/nightly - sudo apt-get update - sudo apt-get install -y --no-install-recommends python3.9-dev python3.9-distutils - name: Install dependencies run: | python -m pip install --upgrade pip From bad7a0207fdd776307f29bc0563fcdc81bd57459 Mon Sep 17 00:00:00 2001 From: symonk Date: Sun, 24 May 2020 11:34:54 +0100 Subject: [PATCH 244/823] document new class instance per test --- doc/en/getting-started.rst | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 56057434edc..55238ffa366 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -153,6 +153,55 @@ Once you develop multiple tests, you may want to group them into a class. pytest The first test passed and the second failed. You can easily see the intermediate values in the assertion to help you understand the reason for the failure. +Some reasons why grouping tests in a class can be useful is: + + * Structural or organizational reasons + * Sharing fixtures for tests only in that particular class + * Applying marks at the class level and having them implicitly apply to all tests + +Something to be aware of when grouping tests inside classes is that each test does not have the same instance of the class. +Having each test share the same class instance would be very detrimental to test isolation and would promote poor test practices. +This is outlined below: + +.. code-block:: python + + class TestClassDemoInstance: + def test_one(self): + assert 0 + + def test_two(self): + assert 0 + + +.. code-block:: pytest + + $ pytest -k TestClassDemoInstance -q + + FF [100%] + ============================================================================================================== FAILURES =============================================================================================================== + ___________________________________________________________________________________________________ TestClassDemoInstance.test_one ____________________________________________________________________________________________________ + + self = , request = > + + def test_one(self, request): + > assert 0 + E assert 0 + + testing\test_example.py:4: AssertionError + ___________________________________________________________________________________________________ TestClassDemoInstance.test_two ____________________________________________________________________________________________________ + + self = , request = > + + def test_two(self, request): + > assert 0 + E assert 0 + + testing\test_example.py:7: AssertionError + ======================================================================================================= short test summary info ======================================================================================================= + FAILED testing/test_example.py::TestClassDemoInstance::test_one - assert 0 + FAILED testing/test_example.py::TestClassDemoInstance::test_two - assert 0 + 2 failed in 0.17s + Request a unique temporary directory for functional tests -------------------------------------------------------------- From 568e00af15ea5e8783a4ee4eccf3ae7575119f6e Mon Sep 17 00:00:00 2001 From: symonk Date: Sun, 24 May 2020 11:43:29 +0100 Subject: [PATCH 245/823] fixing up formatting inline with a smaller shell and typos --- doc/en/getting-started.rst | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 55238ffa366..7dac088922c 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -153,9 +153,9 @@ Once you develop multiple tests, you may want to group them into a class. pytest The first test passed and the second failed. You can easily see the intermediate values in the assertion to help you understand the reason for the failure. -Some reasons why grouping tests in a class can be useful is: +Grouping tests in classes can be beneficial for the following reasons: - * Structural or organizational reasons + * Test organization * Sharing fixtures for tests only in that particular class * Applying marks at the class level and having them implicitly apply to all tests @@ -177,30 +177,32 @@ This is outlined below: $ pytest -k TestClassDemoInstance -q - FF [100%] - ============================================================================================================== FAILURES =============================================================================================================== - ___________________________________________________________________________________________________ TestClassDemoInstance.test_one ____________________________________________________________________________________________________ + FF [100%] + ================================== FAILURES =================================== + _______________________ TestClassDemoInstance.test_one ________________________ - self = , request = > + self = + request = > def test_one(self, request): > assert 0 E assert 0 testing\test_example.py:4: AssertionError - ___________________________________________________________________________________________________ TestClassDemoInstance.test_two ____________________________________________________________________________________________________ + _______________________ TestClassDemoInstance.test_two ________________________ - self = , request = > + self = + request = > def test_two(self, request): > assert 0 E assert 0 testing\test_example.py:7: AssertionError - ======================================================================================================= short test summary info ======================================================================================================= + =========================== short test summary info =========================== FAILED testing/test_example.py::TestClassDemoInstance::test_one - assert 0 FAILED testing/test_example.py::TestClassDemoInstance::test_two - assert 0 - 2 failed in 0.17s + 2 failed in 0.11s Request a unique temporary directory for functional tests -------------------------------------------------------------- From 4f93bc01af057e4bc08bce2c6674c0c7dce95002 Mon Sep 17 00:00:00 2001 From: symonk Date: Sun, 24 May 2020 16:31:51 +0100 Subject: [PATCH 246/823] update terminology of class individuality as per PR feedback --- doc/en/getting-started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 7dac088922c..e8b033fe610 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -159,7 +159,7 @@ Grouping tests in classes can be beneficial for the following reasons: * Sharing fixtures for tests only in that particular class * Applying marks at the class level and having them implicitly apply to all tests -Something to be aware of when grouping tests inside classes is that each test does not have the same instance of the class. +Something to be aware of when grouping tests inside classes is that each has a unique instance of the class. Having each test share the same class instance would be very detrimental to test isolation and would promote poor test practices. This is outlined below: From 5061a47de8d1674a1ad6fa62500def7316a2bab0 Mon Sep 17 00:00:00 2001 From: symonk Date: Sun, 24 May 2020 16:33:17 +0100 Subject: [PATCH 247/823] add missing test text to docs --- doc/en/getting-started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index e8b033fe610..61a0baf1985 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -159,7 +159,7 @@ Grouping tests in classes can be beneficial for the following reasons: * Sharing fixtures for tests only in that particular class * Applying marks at the class level and having them implicitly apply to all tests -Something to be aware of when grouping tests inside classes is that each has a unique instance of the class. +Something to be aware of when grouping tests inside classes is that each test has a unique instance of the class. Having each test share the same class instance would be very detrimental to test isolation and would promote poor test practices. This is outlined below: From 9ee65501813cfc00c3dbe262c08d58377dd05c31 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 24 May 2020 02:12:40 -0400 Subject: [PATCH 248/823] Add in a new hook pytest_warning_recorded for warning capture communication --- doc/en/reference.rst | 1 + src/_pytest/hookspec.py | 33 ++++++++++++++++++++++++++++----- src/_pytest/terminal.py | 7 ++++--- src/_pytest/warnings.py | 17 ++++++++++------- testing/test_warnings.py | 15 +++++++-------- 5 files changed, 50 insertions(+), 23 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 6bc7657c570..13d2484bbf9 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -711,6 +711,7 @@ Session related reporting hooks: .. autofunction:: pytest_fixture_setup .. autofunction:: pytest_fixture_post_finalizer .. autofunction:: pytest_warning_captured +.. autofunction:: pytest_warning_record Central hook for reporting about test execution: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index b4fab332d43..c983f87f9dc 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -622,8 +622,10 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): @hookspec(historic=True) def pytest_warning_captured(warning_message, when, item, location): - """ - Process a warning captured by the internal pytest warnings plugin. + """(**Deprecated**) Process a warning captured by the internal pytest warnings plugin. + + This hook is considered deprecated and will be removed in a future pytest version. + Use :func:`pytest_warning_record` instead. :param warnings.WarningMessage warning_message: The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains @@ -637,9 +639,6 @@ def pytest_warning_captured(warning_message, when, item, location): * ``"runtest"``: during test execution. :param pytest.Item|None item: - **DEPRECATED**: This parameter is incompatible with ``pytest-xdist``, and will always receive ``None`` - in a future release. - The item being executed if ``when`` is ``"runtest"``, otherwise ``None``. :param tuple location: @@ -648,6 +647,30 @@ def pytest_warning_captured(warning_message, when, item, location): """ +@hookspec(historic=True) +def pytest_warning_record(warning_message, when, nodeid, location): + """ + Process a warning captured by the internal pytest warnings plugin. + + :param warnings.WarningMessage warning_message: + The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains + the same attributes as the parameters of :py:func:`warnings.showwarning`. + + :param str when: + Indicates when the warning was captured. Possible values: + + * ``"config"``: during pytest configuration/initialization stage. + * ``"collect"``: during test collection. + * ``"runtest"``: during test execution. + + :param str nodeid: full id of the item + + :param tuple location: + Holds information about the execution context of the captured warning (filename, linenumber, function). + ``function`` evaluates to when the execution context is at the module level. + """ + + # ------------------------------------------------------------------------- # doctest hooks # ------------------------------------------------------------------------- diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 8ecb5a16b63..2524fb21fe1 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -227,7 +227,7 @@ def pytest_report_teststatus(report: TestReport) -> Tuple[str, str, str]: @attr.s class WarningReport: """ - Simple structure to hold warnings information captured by ``pytest_warning_captured``. + Simple structure to hold warnings information captured by ``pytest_warning_record``. :ivar str message: user friendly message about the warning :ivar str|None nodeid: node id that generated the warning (see ``get_location``). @@ -412,13 +412,14 @@ def pytest_internalerror(self, excrepr): return 1 def pytest_warning_captured(self, warning_message, item): - # from _pytest.nodes import get_fslocation_from_item + pass + + def pytest_warning_record(self, warning_message, nodeid): from _pytest.warnings import warning_record_to_str fslocation = warning_message.filename, warning_message.lineno message = warning_record_to_str(warning_message) - nodeid = item.nodeid if item is not None else "" warning_report = WarningReport( fslocation=fslocation, message=message, nodeid=nodeid ) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 527bb03b001..16d8de8f8ba 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -81,7 +81,7 @@ def catch_warnings_for_item(config, ihook, when, item): ``item`` can be None if we are not in the context of an item execution. - Each warning captured triggers the ``pytest_warning_captured`` hook. + Each warning captured triggers the ``pytest_warning_record`` hook. """ cmdline_filters = config.getoption("pythonwarnings") or [] inifilters = config.getini("filterwarnings") @@ -102,6 +102,7 @@ def catch_warnings_for_item(config, ihook, when, item): for arg in cmdline_filters: warnings.filterwarnings(*_parse_filter(arg, escape=True)) + nodeid = "" if item is None else item.nodeid if item is not None: for mark in item.iter_markers(name="filterwarnings"): for arg in mark.args: @@ -110,8 +111,9 @@ def catch_warnings_for_item(config, ihook, when, item): yield for warning_message in log: - ihook.pytest_warning_captured.call_historic( - kwargs=dict(warning_message=warning_message, when=when, item=item) + # raise ValueError(ihook.pytest_warning_record) + ihook.pytest_warning_record.call_historic( + kwargs=dict(warning_message=warning_message, nodeid=nodeid, when=when) ) @@ -166,8 +168,9 @@ def pytest_sessionfinish(session): def _issue_warning_captured(warning, hook, stacklevel): """ This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: - at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured - hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891. + at this point the actual options might not have been set, so we manually trigger the pytest_warning_record hooks + so we can display these warnings in the terminal. + This is a hack until we can sort out #2891. :param warning: the warning instance. :param hook: the hook caller @@ -180,8 +183,8 @@ def _issue_warning_captured(warning, hook, stacklevel): assert records is not None frame = sys._getframe(stacklevel - 1) location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name - hook.pytest_warning_captured.call_historic( + hook.pytest_warning_record.call_historic( kwargs=dict( - warning_message=records[0], when="config", item=None, location=location + warning_message=records[0], when="config", nodeid="", location=location ) ) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 51d1286b465..ec75e657141 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -268,9 +268,8 @@ def test_func(fix): collected = [] class WarningCollector: - def pytest_warning_captured(self, warning_message, when, item): - imge_name = item.name if item is not None else "" - collected.append((str(warning_message.message), when, imge_name)) + def pytest_warning_record(self, warning_message, when, nodeid): + collected.append((str(warning_message.message), when, nodeid)) result = testdir.runpytest(plugins=[WarningCollector()]) result.stdout.fnmatch_lines(["*1 passed*"]) @@ -278,11 +277,11 @@ def pytest_warning_captured(self, warning_message, when, item): expected = [ ("config warning", "config", ""), ("collect warning", "collect", ""), - ("setup warning", "runtest", "test_func"), - ("call warning", "runtest", "test_func"), - ("teardown warning", "runtest", "test_func"), + ("setup warning", "runtest", "test_warning_captured_hook.py::test_func"), + ("call warning", "runtest", "test_warning_captured_hook.py::test_func"), + ("teardown warning", "runtest", "test_warning_captured_hook.py::test_func"), ] - assert collected == expected + assert collected == expected, str(collected) @pytest.mark.filterwarnings("always") @@ -649,7 +648,7 @@ class CapturedWarnings: captured = [] @classmethod - def pytest_warning_captured(cls, warning_message, when, item, location): + def pytest_warning_record(cls, warning_message, when, nodeid, location): cls.captured.append((warning_message, location)) testdir.plugins = [CapturedWarnings()] From b02d087dbdd6f8b0a4233314356b2442da7fc6c5 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 24 May 2020 20:26:14 -0400 Subject: [PATCH 249/823] cleanup code pre pr --- src/_pytest/warnings.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 16d8de8f8ba..4aa2321aa10 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -111,7 +111,6 @@ def catch_warnings_for_item(config, ihook, when, item): yield for warning_message in log: - # raise ValueError(ihook.pytest_warning_record) ihook.pytest_warning_record.call_historic( kwargs=dict(warning_message=warning_message, nodeid=nodeid, when=when) ) @@ -168,9 +167,8 @@ def pytest_sessionfinish(session): def _issue_warning_captured(warning, hook, stacklevel): """ This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: - at this point the actual options might not have been set, so we manually trigger the pytest_warning_record hooks - so we can display these warnings in the terminal. - This is a hack until we can sort out #2891. + at this point the actual options might not have been set, so we manually trigger the pytest_warning_record + hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891. :param warning: the warning instance. :param hook: the hook caller From 088d400b2da633c3eaa7e2b9b6edef18f88dd885 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 24 May 2020 20:43:23 -0400 Subject: [PATCH 250/823] rename pytest_warning_record -> pytest_warning_recorded --- doc/en/reference.rst | 2 +- src/_pytest/hookspec.py | 4 ++-- src/_pytest/terminal.py | 4 ++-- src/_pytest/warnings.py | 8 ++++---- testing/test_warnings.py | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 13d2484bbf9..7348636a2dd 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -711,7 +711,7 @@ Session related reporting hooks: .. autofunction:: pytest_fixture_setup .. autofunction:: pytest_fixture_post_finalizer .. autofunction:: pytest_warning_captured -.. autofunction:: pytest_warning_record +.. autofunction:: pytest_warning_recorded Central hook for reporting about test execution: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index c983f87f9dc..8ccb89ca550 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -625,7 +625,7 @@ def pytest_warning_captured(warning_message, when, item, location): """(**Deprecated**) Process a warning captured by the internal pytest warnings plugin. This hook is considered deprecated and will be removed in a future pytest version. - Use :func:`pytest_warning_record` instead. + Use :func:`pytest_warning_recorded` instead. :param warnings.WarningMessage warning_message: The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains @@ -648,7 +648,7 @@ def pytest_warning_captured(warning_message, when, item, location): @hookspec(historic=True) -def pytest_warning_record(warning_message, when, nodeid, location): +def pytest_warning_recorded(warning_message, when, nodeid, location): """ Process a warning captured by the internal pytest warnings plugin. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2524fb21fe1..10fb1f76945 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -227,7 +227,7 @@ def pytest_report_teststatus(report: TestReport) -> Tuple[str, str, str]: @attr.s class WarningReport: """ - Simple structure to hold warnings information captured by ``pytest_warning_record``. + Simple structure to hold warnings information captured by ``pytest_warning_recorded``. :ivar str message: user friendly message about the warning :ivar str|None nodeid: node id that generated the warning (see ``get_location``). @@ -414,7 +414,7 @@ def pytest_internalerror(self, excrepr): def pytest_warning_captured(self, warning_message, item): pass - def pytest_warning_record(self, warning_message, nodeid): + def pytest_warning_recorded(self, warning_message, nodeid): from _pytest.warnings import warning_record_to_str fslocation = warning_message.filename, warning_message.lineno diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 4aa2321aa10..6d383ccddcd 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -81,7 +81,7 @@ def catch_warnings_for_item(config, ihook, when, item): ``item`` can be None if we are not in the context of an item execution. - Each warning captured triggers the ``pytest_warning_record`` hook. + Each warning captured triggers the ``pytest_warning_recorded`` hook. """ cmdline_filters = config.getoption("pythonwarnings") or [] inifilters = config.getini("filterwarnings") @@ -111,7 +111,7 @@ def catch_warnings_for_item(config, ihook, when, item): yield for warning_message in log: - ihook.pytest_warning_record.call_historic( + ihook.pytest_warning_recorded.call_historic( kwargs=dict(warning_message=warning_message, nodeid=nodeid, when=when) ) @@ -167,7 +167,7 @@ def pytest_sessionfinish(session): def _issue_warning_captured(warning, hook, stacklevel): """ This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: - at this point the actual options might not have been set, so we manually trigger the pytest_warning_record + at this point the actual options might not have been set, so we manually trigger the pytest_warning_recorded hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891. :param warning: the warning instance. @@ -181,7 +181,7 @@ def _issue_warning_captured(warning, hook, stacklevel): assert records is not None frame = sys._getframe(stacklevel - 1) location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name - hook.pytest_warning_record.call_historic( + hook.pytest_warning_recorded.call_historic( kwargs=dict( warning_message=records[0], when="config", nodeid="", location=location ) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index ec75e657141..6cfdfa6bb81 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -268,7 +268,7 @@ def test_func(fix): collected = [] class WarningCollector: - def pytest_warning_record(self, warning_message, when, nodeid): + def pytest_warning_recorded(self, warning_message, when, nodeid): collected.append((str(warning_message.message), when, nodeid)) result = testdir.runpytest(plugins=[WarningCollector()]) @@ -648,7 +648,7 @@ class CapturedWarnings: captured = [] @classmethod - def pytest_warning_record(cls, warning_message, when, nodeid, location): + def pytest_warning_recorded(cls, warning_message, when, nodeid, location): cls.captured.append((warning_message, location)) testdir.plugins = [CapturedWarnings()] From 6546d1f7257d986c2598dd2ebd2bbd8d04285585 Mon Sep 17 00:00:00 2001 From: Florian Dahlitz Date: Mon, 25 May 2020 13:53:56 +0200 Subject: [PATCH 251/823] Prevent pytest from printing ConftestImportFailure traceback --- changelog/6956.bugfix.rst | 1 + src/_pytest/nodes.py | 3 +++ testing/python/collect.py | 9 +++------ testing/test_reports.py | 12 ++++++++++++ 4 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 changelog/6956.bugfix.rst diff --git a/changelog/6956.bugfix.rst b/changelog/6956.bugfix.rst new file mode 100644 index 00000000000..a88ef94b6d5 --- /dev/null +++ b/changelog/6956.bugfix.rst @@ -0,0 +1 @@ +Prevent pytest from printing ConftestImportFailure traceback to stdout. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 2f5f9bdb801..4c2a0a3a793 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -19,6 +19,7 @@ from _pytest.compat import cached_property from _pytest.compat import TYPE_CHECKING from _pytest.config import Config +from _pytest.config import ConftestImportFailure from _pytest.config import PytestPluginManager from _pytest.deprecated import NODE_USE_FROM_PARENT from _pytest.fixtures import FixtureDef @@ -340,6 +341,8 @@ def _repr_failure_py( return excinfo.value.formatrepr() if self.config.getoption("fulltrace", False): style = "long" + if excinfo.type is ConftestImportFailure: # type: ignore + excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo) # type: ignore else: tb = _pytest._code.Traceback([excinfo.traceback[-1]]) self._prunetraceback(excinfo) diff --git a/testing/python/collect.py b/testing/python/collect.py index 2807cacc90a..cbc798ad8e0 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1251,7 +1251,7 @@ def test_syntax_error_with_non_ascii_chars(testdir): result.stdout.fnmatch_lines(["*ERROR collecting*", "*SyntaxError*", "*1 error in*"]) -def test_collecterror_with_fulltrace(testdir): +def test_collect_error_with_fulltrace(testdir): testdir.makepyfile("assert 0") result = testdir.runpytest("--fulltrace") result.stdout.fnmatch_lines( @@ -1259,15 +1259,12 @@ def test_collecterror_with_fulltrace(testdir): "collected 0 items / 1 error", "", "*= ERRORS =*", - "*_ ERROR collecting test_collecterror_with_fulltrace.py _*", - "", - "*/_pytest/python.py:*: ", - "_ _ _ _ _ _ _ _ *", + "*_ ERROR collecting test_collect_error_with_fulltrace.py _*", "", "> assert 0", "E assert 0", "", - "test_collecterror_with_fulltrace.py:1: AssertionError", + "test_collect_error_with_fulltrace.py:1: AssertionError", "*! Interrupted: 1 error during collection !*", ] ) diff --git a/testing/test_reports.py b/testing/test_reports.py index 13f5932156b..64d86e9534b 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -396,6 +396,18 @@ def check_longrepr(longrepr): # for same reasons as previous test, ensure we don't blow up here loaded_report.longrepr.toterminal(tw_mock) + def test_report_prevent_ConftestImportFailure_hiding_exception(self, testdir): + sub_dir = testdir.tmpdir.join("ns").ensure_dir() + sub_dir.join("conftest").new(ext=".py").write("import unknown") + + result = testdir.runpytest_subprocess(".") + result.stdout.fnmatch_lines( + ["E ModuleNotFoundError: No module named 'unknown'"] + ) + result.stdout.no_fnmatch_line( + "ERROR - _pytest.config.ConftestImportFailure: ModuleNotFoundError:*" + ) + class TestHooks: """Test that the hooks are working correctly for plugins""" From 125b663f20a599e7e39fce821597a122d62a6460 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Mon, 25 May 2020 11:18:24 -0400 Subject: [PATCH 252/823] Address all feedback, minus the empty sring v None nodeid which is being discussed --- src/_pytest/hookspec.py | 7 ++++++- src/_pytest/terminal.py | 3 --- src/_pytest/warnings.py | 8 ++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 8ccb89ca550..e2810ec78bc 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -620,7 +620,12 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): """ -@hookspec(historic=True) +@hookspec( + historic=True, + warn_on_impl=DeprecationWarning( + "pytest_warning_captured is deprecated and will be removed soon" + ), +) def pytest_warning_captured(warning_message, when, item, location): """(**Deprecated**) Process a warning captured by the internal pytest warnings plugin. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 10fb1f76945..d26db2c510b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -411,9 +411,6 @@ def pytest_internalerror(self, excrepr): self.write_line("INTERNALERROR> " + line) return 1 - def pytest_warning_captured(self, warning_message, item): - pass - def pytest_warning_recorded(self, warning_message, nodeid): from _pytest.warnings import warning_record_to_str diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 6d383ccddcd..3ae3c863929 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -111,6 +111,9 @@ def catch_warnings_for_item(config, ihook, when, item): yield for warning_message in log: + ihook.pytest_warning_captured.call_historic( + kwargs=dict(warning_message=warning_message, when=when, item=item) + ) ihook.pytest_warning_recorded.call_historic( kwargs=dict(warning_message=warning_message, nodeid=nodeid, when=when) ) @@ -181,6 +184,11 @@ def _issue_warning_captured(warning, hook, stacklevel): assert records is not None frame = sys._getframe(stacklevel - 1) location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + hook.pytest_warning_captured.call_historic( + kwargs=dict( + warning_message=records[0], when="config", item=None, location=location + ) + ) hook.pytest_warning_recorded.call_historic( kwargs=dict( warning_message=records[0], when="config", nodeid="", location=location From 5ebcb34fb595c7652ccda889f69b2d4e3ca6443b Mon Sep 17 00:00:00 2001 From: Florian Dahlitz Date: Mon, 25 May 2020 20:19:28 +0200 Subject: [PATCH 253/823] Move ConftestImportFailure check to correct position and add typing --- src/_pytest/nodes.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 4c2a0a3a793..5aae211cd7e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -332,17 +332,21 @@ def _prunetraceback(self, excinfo): pass def _repr_failure_py( - self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]], style=None + self, + excinfo: ExceptionInfo[ + Union[Failed, FixtureLookupError, ConftestImportFailure] + ], + style=None, ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: if isinstance(excinfo.value, fail.Exception): if not excinfo.value.pytrace: return str(excinfo.value) if isinstance(excinfo.value, FixtureLookupError): return excinfo.value.formatrepr() + if isinstance(excinfo.value, ConftestImportFailure): + excinfo = ExceptionInfo(excinfo.value.excinfo) # type: ignore if self.config.getoption("fulltrace", False): style = "long" - if excinfo.type is ConftestImportFailure: # type: ignore - excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo) # type: ignore else: tb = _pytest._code.Traceback([excinfo.traceback[-1]]) self._prunetraceback(excinfo) From 491239d9b27670d55cee21f9141a2bc91cf44f16 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 12 Apr 2020 20:47:28 +0300 Subject: [PATCH 254/823] capture: remove some indirection in MultiCapture Removing this indirection enables some further clean ups. --- src/_pytest/capture.py | 22 +++++++++------------- src/_pytest/pytester.py | 5 ++--- testing/test_capture.py | 37 ++++++++++++++++++++++++++++--------- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 323881151d9..201bcd962fe 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -71,13 +71,13 @@ def pytest_load_initial_conftests(early_config: Config): def _get_multicapture(method: "_CaptureMethod") -> "MultiCapture": if method == "fd": - return MultiCapture(out=True, err=True, Capture=FDCapture) + return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) elif method == "sys": - return MultiCapture(out=True, err=True, Capture=SysCapture) + return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2)) elif method == "no": - return MultiCapture(out=False, err=False, in_=False) + return MultiCapture(in_=None, out=None, err=None) elif method == "tee-sys": - return MultiCapture(out=True, err=True, in_=False, Capture=TeeSysCapture) + return MultiCapture(in_=None, out=TeeSysCapture(1), err=TeeSysCapture(2)) raise ValueError("unknown capturing method: {!r}".format(method)) @@ -354,7 +354,7 @@ def __init__(self, captureclass, request): def _start(self): if self._capture is None: self._capture = MultiCapture( - out=True, err=True, in_=False, Capture=self.captureclass + in_=None, out=self.captureclass(1), err=self.captureclass(2), ) self._capture.start_capturing() @@ -418,17 +418,13 @@ def mode(self) -> str: class MultiCapture: - out = err = in_ = None _state = None _in_suspended = False - def __init__(self, out=True, err=True, in_=True, Capture=None): - if in_: - self.in_ = Capture(0) - if out: - self.out = Capture(1) - if err: - self.err = Capture(2) + def __init__(self, in_, out, err) -> None: + self.in_ = in_ + self.out = out + self.err = err def __repr__(self): return "".format( diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 1da4787361c..9df86a22fa3 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -25,8 +25,7 @@ import pytest from _pytest._code import Source -from _pytest.capture import MultiCapture -from _pytest.capture import SysCapture +from _pytest.capture import _get_multicapture from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin from _pytest.config import Config @@ -972,7 +971,7 @@ def runpytest_inprocess(self, *args, **kwargs) -> RunResult: if syspathinsert: self.syspathinsert() now = time.time() - capture = MultiCapture(Capture=SysCapture) + capture = _get_multicapture("sys") capture.start_capturing() try: try: diff --git a/testing/test_capture.py b/testing/test_capture.py index 177d72ebc3d..fa4f523d958 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -19,16 +19,28 @@ # pylib 1.4.20.dev2 (rev 13d9af95547e) -def StdCaptureFD(out=True, err=True, in_=True): - return capture.MultiCapture(out, err, in_, Capture=capture.FDCapture) +def StdCaptureFD(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture: + return capture.MultiCapture( + in_=capture.FDCapture(0) if in_ else None, + out=capture.FDCapture(1) if out else None, + err=capture.FDCapture(2) if err else None, + ) -def StdCapture(out=True, err=True, in_=True): - return capture.MultiCapture(out, err, in_, Capture=capture.SysCapture) +def StdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture: + return capture.MultiCapture( + in_=capture.SysCapture(0) if in_ else None, + out=capture.SysCapture(1) if out else None, + err=capture.SysCapture(2) if err else None, + ) -def TeeStdCapture(out=True, err=True, in_=True): - return capture.MultiCapture(out, err, in_, Capture=capture.TeeSysCapture) +def TeeStdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture: + return capture.MultiCapture( + in_=capture.TeeSysCapture(0) if in_ else None, + out=capture.TeeSysCapture(1) if out else None, + err=capture.TeeSysCapture(2) if err else None, + ) class TestCaptureManager: @@ -1154,7 +1166,11 @@ def test_stdcapture_fd_invalid_fd(self, testdir): from _pytest import capture def StdCaptureFD(out=True, err=True, in_=True): - return capture.MultiCapture(out, err, in_, Capture=capture.FDCapture) + return capture.MultiCapture( + in_=capture.FDCapture(0) if in_ else None, + out=capture.FDCapture(1) if out else None, + err=capture.FDCapture(2) if err else None, + ) def test_stdout(): os.close(1) @@ -1284,8 +1300,11 @@ def test_capturing_and_logging_fundamentals(testdir, method): import sys, os import py, logging from _pytest import capture - cap = capture.MultiCapture(out=False, in_=False, - Capture=capture.%s) + cap = capture.MultiCapture( + in_=None, + out=None, + err=capture.%s(2), + ) cap.start_capturing() logging.warning("hello1") From 2695b41df3a0f69023c1736da45a9dbe02e8340c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 12 Apr 2020 21:09:39 +0300 Subject: [PATCH 255/823] capture: inline _capturing_for_request to simplify the control flow With straight code, it is a little easier to understand, and simplify further. --- src/_pytest/capture.py | 75 +++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 201bcd962fe..3ff9455b0f1 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -9,14 +9,12 @@ import sys from io import UnsupportedOperation from tempfile import TemporaryFile -from typing import Generator from typing import Optional from typing import TextIO import pytest from _pytest.compat import TYPE_CHECKING from _pytest.config import Config -from _pytest.fixtures import FixtureRequest if TYPE_CHECKING: from typing_extensions import Literal @@ -150,35 +148,20 @@ def resume(self): def read_global_capture(self): return self._global_capturing.readouterr() - # Fixture Control (it's just forwarding, think about removing this later) + # Fixture Control - @contextlib.contextmanager - def _capturing_for_request( - self, request: FixtureRequest - ) -> Generator["CaptureFixture", None, None]: - """ - Context manager that creates a ``CaptureFixture`` instance for the - given ``request``, ensuring there is only a single one being requested - at the same time. - - This is used as a helper with ``capsys``, ``capfd`` etc. - """ + def set_fixture(self, capture_fixture: "CaptureFixture") -> None: if self._capture_fixture: - other_name = next( - k - for k, v in map_fixname_class.items() - if v is self._capture_fixture.captureclass - ) - raise request.raiseerror( + current_fixture = self._capture_fixture.request.fixturename + requested_fixture = capture_fixture.request.fixturename + capture_fixture.request.raiseerror( "cannot use {} and {} at the same time".format( - request.fixturename, other_name + requested_fixture, current_fixture ) ) - capture_class = map_fixname_class[request.fixturename] - self._capture_fixture = CaptureFixture(capture_class, request) - self.activate_fixture() - yield self._capture_fixture - self._capture_fixture.close() + self._capture_fixture = capture_fixture + + def unset_fixture(self) -> None: self._capture_fixture = None def activate_fixture(self): @@ -276,8 +259,12 @@ def capsys(request): ``out`` and ``err`` will be ``text`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - with capman._capturing_for_request(request) as fixture: - yield fixture + capture_fixture = CaptureFixture(SysCapture, request) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() @pytest.fixture @@ -289,8 +276,12 @@ def capsysbinary(request): ``out`` and ``err`` will be ``bytes`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - with capman._capturing_for_request(request) as fixture: - yield fixture + capture_fixture = CaptureFixture(SysCaptureBinary, request) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() @pytest.fixture @@ -302,8 +293,12 @@ def capfd(request): ``out`` and ``err`` will be ``text`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - with capman._capturing_for_request(request) as fixture: - yield fixture + capture_fixture = CaptureFixture(FDCapture, request) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() @pytest.fixture @@ -315,8 +310,12 @@ def capfdbinary(request): ``out`` and ``err`` will be ``byte`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - with capman._capturing_for_request(request) as fixture: - yield fixture + capture_fixture = CaptureFixture(FDCaptureBinary, request) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() class CaptureIO(io.TextIOWrapper): @@ -701,14 +700,6 @@ def __init__(self, fd, tmpfile=None): self.tmpfile = tmpfile -map_fixname_class = { - "capfd": FDCapture, - "capfdbinary": FDCaptureBinary, - "capsys": SysCapture, - "capsysbinary": SysCaptureBinary, -} - - class DontReadFromInput: encoding = None From 02c95ea624eded63b64611aee28a0b1e0bb4ad69 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 15 Apr 2020 16:24:07 +0300 Subject: [PATCH 256/823] capture: remove unused FDCapture tmpfile argument --- src/_pytest/capture.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 3ff9455b0f1..106b3fafb4d 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -506,7 +506,7 @@ class FDCaptureBinary: EMPTY_BUFFER = b"" _state = None - def __init__(self, targetfd, tmpfile=None): + def __init__(self, targetfd): self.targetfd = targetfd try: @@ -530,22 +530,19 @@ def __init__(self, targetfd, tmpfile=None): self.targetfd_save = os.dup(targetfd) if targetfd == 0: - assert not tmpfile, "cannot set tmpfile with stdin" - tmpfile = open(os.devnull) + self.tmpfile = open(os.devnull) self.syscapture = SysCapture(targetfd) else: - if tmpfile is None: - tmpfile = EncodedFile( - TemporaryFile(buffering=0), - encoding="utf-8", - errors="replace", - write_through=True, - ) + self.tmpfile = EncodedFile( + TemporaryFile(buffering=0), + encoding="utf-8", + errors="replace", + write_through=True, + ) if targetfd in patchsysdict: - self.syscapture = SysCapture(targetfd, tmpfile) + self.syscapture = SysCapture(targetfd, self.tmpfile) else: self.syscapture = NoCapture() - self.tmpfile = tmpfile def __repr__(self): return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format( From ea3f44894f8d0f944f11b782dd722232a97092ca Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 16 Apr 2020 09:49:17 +0300 Subject: [PATCH 257/823] capture: replace TeeSysCapture with SysCapture(tee=True) This is more straightforward and does not require duplicating the initialization logic. --- src/_pytest/capture.py | 21 +++++---------------- testing/test_capture.py | 14 ++++++++------ 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 106b3fafb4d..1eb5e1b417f 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -75,7 +75,9 @@ def _get_multicapture(method: "_CaptureMethod") -> "MultiCapture": elif method == "no": return MultiCapture(in_=None, out=None, err=None) elif method == "tee-sys": - return MultiCapture(in_=None, out=TeeSysCapture(1), err=TeeSysCapture(2)) + return MultiCapture( + in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True) + ) raise ValueError("unknown capturing method: {!r}".format(method)) @@ -620,7 +622,7 @@ class SysCaptureBinary: EMPTY_BUFFER = b"" _state = None - def __init__(self, fd, tmpfile=None): + def __init__(self, fd, tmpfile=None, *, tee=False): name = patchsysdict[fd] self._old = getattr(sys, name) self.name = name @@ -628,7 +630,7 @@ def __init__(self, fd, tmpfile=None): if name == "stdin": tmpfile = DontReadFromInput() else: - tmpfile = CaptureIO() + tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old) self.tmpfile = tmpfile def __repr__(self): @@ -684,19 +686,6 @@ def writeorg(self, data): self._old.flush() -class TeeSysCapture(SysCapture): - def __init__(self, fd, tmpfile=None): - name = patchsysdict[fd] - self._old = getattr(sys, name) - self.name = name - if tmpfile is None: - if name == "stdin": - tmpfile = DontReadFromInput() - else: - tmpfile = TeeCaptureIO(self._old) - self.tmpfile = tmpfile - - class DontReadFromInput: encoding = None diff --git a/testing/test_capture.py b/testing/test_capture.py index fa4f523d958..5a0998da7ae 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -37,9 +37,9 @@ def StdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCap def TeeStdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture: return capture.MultiCapture( - in_=capture.TeeSysCapture(0) if in_ else None, - out=capture.TeeSysCapture(1) if out else None, - err=capture.TeeSysCapture(2) if err else None, + in_=capture.SysCapture(0, tee=True) if in_ else None, + out=capture.SysCapture(1, tee=True) if out else None, + err=capture.SysCapture(2, tee=True) if err else None, ) @@ -1292,8 +1292,10 @@ def test_capture_again(): ) -@pytest.mark.parametrize("method", ["SysCapture", "FDCapture", "TeeSysCapture"]) -def test_capturing_and_logging_fundamentals(testdir, method): +@pytest.mark.parametrize( + "method", ["SysCapture(2)", "SysCapture(2, tee=True)", "FDCapture(2)"] +) +def test_capturing_and_logging_fundamentals(testdir, method: str) -> None: # here we check a fundamental feature p = testdir.makepyfile( """ @@ -1303,7 +1305,7 @@ def test_capturing_and_logging_fundamentals(testdir, method): cap = capture.MultiCapture( in_=None, out=None, - err=capture.%s(2), + err=capture.%s, ) cap.start_capturing() From 95bd232e57603329e16bb3a4c694e4fe78655c7b Mon Sep 17 00:00:00 2001 From: Florian Dahlitz Date: Tue, 26 May 2020 10:31:53 +0200 Subject: [PATCH 258/823] Apply suggestions from @bluetech --- src/_pytest/nodes.py | 11 +++-------- testing/test_reports.py | 4 +--- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 5aae211cd7e..2ed25061063 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -29,7 +29,6 @@ from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords from _pytest.outcomes import fail -from _pytest.outcomes import Failed from _pytest.store import Store if TYPE_CHECKING: @@ -332,19 +331,15 @@ def _prunetraceback(self, excinfo): pass def _repr_failure_py( - self, - excinfo: ExceptionInfo[ - Union[Failed, FixtureLookupError, ConftestImportFailure] - ], - style=None, + self, excinfo: ExceptionInfo[BaseException], style=None, ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: + if isinstance(excinfo.value, ConftestImportFailure): + excinfo = ExceptionInfo(excinfo.value.excinfo) if isinstance(excinfo.value, fail.Exception): if not excinfo.value.pytrace: return str(excinfo.value) if isinstance(excinfo.value, FixtureLookupError): return excinfo.value.formatrepr() - if isinstance(excinfo.value, ConftestImportFailure): - excinfo = ExceptionInfo(excinfo.value.excinfo) # type: ignore if self.config.getoption("fulltrace", False): style = "long" else: diff --git a/testing/test_reports.py b/testing/test_reports.py index 64d86e9534b..9e4e7d09de3 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -404,9 +404,7 @@ def test_report_prevent_ConftestImportFailure_hiding_exception(self, testdir): result.stdout.fnmatch_lines( ["E ModuleNotFoundError: No module named 'unknown'"] ) - result.stdout.no_fnmatch_line( - "ERROR - _pytest.config.ConftestImportFailure: ModuleNotFoundError:*" - ) + result.stdout.no_fnmatch_line("ERROR - *ConftestImportFailure*") class TestHooks: From 5507752c530033865986880c93154465aac53e92 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:15 +0300 Subject: [PATCH 259/823] fixtures: remove special cases when deciding when pytest.fixture() is a direct decoration pytest.fixture() can be used either as @pytest.fixture def func(): ... or as @pytest.fixture() def func(): ... or (while maybe not intended) func = pytest.fixture(func) so it needs to inspect internally whether it got a function in the first positional argument or not. Previously, there were was oddity. In the following, func = pytest.fixture(func, autouse=True) # OR func = pytest.fixture(func, parms=['a', 'b']) The result is as if `func` wasn't passed. There isn't any reason for this special that I can understand, so remove it. --- changelog/7253.bugfix.rst | 3 +++ src/_pytest/fixtures.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 changelog/7253.bugfix.rst diff --git a/changelog/7253.bugfix.rst b/changelog/7253.bugfix.rst new file mode 100644 index 00000000000..e73ef663f00 --- /dev/null +++ b/changelog/7253.bugfix.rst @@ -0,0 +1,3 @@ +When using ``pytest.fixture`` on a function directly, as in ``pytest.fixture(func)``, +if the ``autouse`` or ``params`` arguments are also passed, the function is no longer +ignored, but is marked as a fixture. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 82a148127d3..a1574634a01 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1152,13 +1152,15 @@ def fixture( if params is not None: params = list(params) - if fixture_function and params is None and autouse is False: - # direct decoration - return FixtureFunctionMarker(scope, params, autouse, name=name)( - fixture_function - ) + fixture_marker = FixtureFunctionMarker( + scope=scope, params=params, autouse=autouse, ids=ids, name=name, + ) + + # Direct decoration. + if fixture_function: + return fixture_marker(fixture_function) - return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) + return fixture_marker def yield_fixture( From aca534c67dea7eb0fcddf194bc64d65bc3e07c8b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 26 May 2020 14:59:16 +0300 Subject: [PATCH 260/823] Improve our own wcwidth implementation and remove dependency on wcwidth package `TerminalWriter`, imported recently from `py`, contains its own incomplete wcwidth (`char_with`/`get_line_width`) implementation. The `TerminalReporter` also needs this, but uses the external `wcwidth` package. This commit brings the `TerminalWriter` implementation up-to-par with `wcwidth`, moves to implementation to a new file `_pytest._io.wcwidth` which is used everywhere, and removes the dependency. The differences compared to the `wcwidth` package are: - Normalizes the string before counting. - Uses Python's `unicodedata` instead of vendored Unicode tables. This means the data corresponds to the Python's version Unicode version instead of the `wcwidth`'s package version. - Apply some optimizations. --- changelog/7264.improvement.rst | 1 + setup.py | 1 - src/_pytest/_io/terminalwriter.py | 17 ++-------- src/_pytest/_io/wcwidth.py | 55 +++++++++++++++++++++++++++++++ src/_pytest/terminal.py | 3 +- testing/io/test_wcwidth.py | 38 +++++++++++++++++++++ testing/test_terminal.py | 27 ++++++++------- 7 files changed, 111 insertions(+), 31 deletions(-) create mode 100644 changelog/7264.improvement.rst create mode 100644 src/_pytest/_io/wcwidth.py create mode 100644 testing/io/test_wcwidth.py diff --git a/changelog/7264.improvement.rst b/changelog/7264.improvement.rst new file mode 100644 index 00000000000..035745c4dd9 --- /dev/null +++ b/changelog/7264.improvement.rst @@ -0,0 +1 @@ +The dependency on the ``wcwidth`` package has been removed. diff --git a/setup.py b/setup.py index 6ebfd67fbf5..cd2ecbe07f7 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,6 @@ 'colorama;sys_platform=="win32"', "pluggy>=0.12,<1.0", 'importlib-metadata>=0.12;python_version<"3.8"', - "wcwidth", ] diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 4f22f5a7ad7..a285cf4fc36 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -2,12 +2,12 @@ import os import shutil import sys -import unicodedata -from functools import lru_cache from typing import Optional from typing import Sequence from typing import TextIO +from .wcwidth import wcswidth + # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. @@ -22,17 +22,6 @@ def get_terminal_width() -> int: return width -@lru_cache(100) -def char_width(c: str) -> int: - # Fullwidth and Wide -> 2, all else (including Ambiguous) -> 1. - return 2 if unicodedata.east_asian_width(c) in ("F", "W") else 1 - - -def get_line_width(text: str) -> int: - text = unicodedata.normalize("NFC", text) - return sum(char_width(c) for c in text) - - def should_do_markup(file: TextIO) -> bool: if os.environ.get("PY_COLORS") == "1": return True @@ -99,7 +88,7 @@ def fullwidth(self, value: int) -> None: @property def width_of_current_line(self) -> int: """Return an estimate of the width so far in the current line.""" - return get_line_width(self._current_line) + return wcswidth(self._current_line) def markup(self, text: str, **markup: bool) -> str: for name in markup: diff --git a/src/_pytest/_io/wcwidth.py b/src/_pytest/_io/wcwidth.py new file mode 100644 index 00000000000..e5c7bf4d868 --- /dev/null +++ b/src/_pytest/_io/wcwidth.py @@ -0,0 +1,55 @@ +import unicodedata +from functools import lru_cache + + +@lru_cache(100) +def wcwidth(c: str) -> int: + """Determine how many columns are needed to display a character in a terminal. + + Returns -1 if the character is not printable. + Returns 0, 1 or 2 for other characters. + """ + o = ord(c) + + # ASCII fast path. + if 0x20 <= o < 0x07F: + return 1 + + # Some Cf/Zp/Zl characters which should be zero-width. + if ( + o == 0x0000 + or 0x200B <= o <= 0x200F + or 0x2028 <= o <= 0x202E + or 0x2060 <= o <= 0x2063 + ): + return 0 + + category = unicodedata.category(c) + + # Control characters. + if category == "Cc": + return -1 + + # Combining characters with zero width. + if category in ("Me", "Mn"): + return 0 + + # Full/Wide east asian characters. + if unicodedata.east_asian_width(c) in ("F", "W"): + return 2 + + return 1 + + +def wcswidth(s: str) -> int: + """Determine how many columns are needed to display a string in a terminal. + + Returns -1 if the string contains non-printable characters. + """ + width = 0 + for c in unicodedata.normalize("NFC", s): + wc = wcwidth(c) + if wc < 0: + return -1 + width += wc + return width diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 8ecb5a16b63..646fe4cca3f 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -27,6 +27,7 @@ import pytest from _pytest import nodes from _pytest._io import TerminalWriter +from _pytest._io.wcwidth import wcswidth from _pytest.compat import order_preserving_dict from _pytest.config import Config from _pytest.config import ExitCode @@ -1122,8 +1123,6 @@ def _get_pos(config, rep): def _get_line_with_reprcrash_message(config, rep, termwidth): """Get summary line for a report, trying to add reprcrash message.""" - from wcwidth import wcswidth - verbose_word = rep._get_verbose_word(config) pos = _get_pos(config, rep) diff --git a/testing/io/test_wcwidth.py b/testing/io/test_wcwidth.py new file mode 100644 index 00000000000..7cc74df5d07 --- /dev/null +++ b/testing/io/test_wcwidth.py @@ -0,0 +1,38 @@ +import pytest +from _pytest._io.wcwidth import wcswidth +from _pytest._io.wcwidth import wcwidth + + +@pytest.mark.parametrize( + ("c", "expected"), + [ + ("\0", 0), + ("\n", -1), + ("a", 1), + ("1", 1), + ("א", 1), + ("\u200B", 0), + ("\u1ABE", 0), + ("\u0591", 0), + ("🉐", 2), + ("$", 2), + ], +) +def test_wcwidth(c: str, expected: int) -> None: + assert wcwidth(c) == expected + + +@pytest.mark.parametrize( + ("s", "expected"), + [ + ("", 0), + ("hello, world!", 13), + ("hello, world!\n", -1), + ("0123456789", 10), + ("שלום, עולם!", 11), + ("שְבֻעָיים", 6), + ("🉐🉐🉐", 6), + ], +) +def test_wcswidth(s: str, expected: int) -> None: + assert wcswidth(s) == expected diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 0f5b4cb689a..17fd29238f7 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -14,7 +14,9 @@ import py import _pytest.config +import _pytest.terminal import pytest +from _pytest._io.wcwidth import wcswidth from _pytest.config import ExitCode from _pytest.pytester import Testdir from _pytest.reports import BaseReport @@ -2027,9 +2029,6 @@ class X: def test_line_with_reprcrash(monkeypatch): - import _pytest.terminal - from wcwidth import wcswidth - mocked_verbose_word = "FAILED" mocked_pos = "some::nodeid" @@ -2079,19 +2078,19 @@ def check(msg, width, expected): check("some\nmessage", 80, "FAILED some::nodeid - some") # Test unicode safety. - check("😄😄😄😄😄\n2nd line", 25, "FAILED some::nodeid - ...") - check("😄😄😄😄😄\n2nd line", 26, "FAILED some::nodeid - ...") - check("😄😄😄😄😄\n2nd line", 27, "FAILED some::nodeid - 😄...") - check("😄😄😄😄😄\n2nd line", 28, "FAILED some::nodeid - 😄...") - check("😄😄😄😄😄\n2nd line", 29, "FAILED some::nodeid - 😄😄...") + check("🉐🉐🉐🉐🉐\n2nd line", 25, "FAILED some::nodeid - ...") + check("🉐🉐🉐🉐🉐\n2nd line", 26, "FAILED some::nodeid - ...") + check("🉐🉐🉐🉐🉐\n2nd line", 27, "FAILED some::nodeid - 🉐...") + check("🉐🉐🉐🉐🉐\n2nd line", 28, "FAILED some::nodeid - 🉐...") + check("🉐🉐🉐🉐🉐\n2nd line", 29, "FAILED some::nodeid - 🉐🉐...") # NOTE: constructed, not sure if this is supported. - mocked_pos = "nodeid::😄::withunicode" - check("😄😄😄😄😄\n2nd line", 29, "FAILED nodeid::😄::withunicode") - check("😄😄😄😄😄\n2nd line", 40, "FAILED nodeid::😄::withunicode - 😄😄...") - check("😄😄😄😄😄\n2nd line", 41, "FAILED nodeid::😄::withunicode - 😄😄...") - check("😄😄😄😄😄\n2nd line", 42, "FAILED nodeid::😄::withunicode - 😄😄😄...") - check("😄😄😄😄😄\n2nd line", 80, "FAILED nodeid::😄::withunicode - 😄😄😄😄😄") + mocked_pos = "nodeid::🉐::withunicode" + check("🉐🉐🉐🉐🉐\n2nd line", 29, "FAILED nodeid::🉐::withunicode") + check("🉐🉐🉐🉐🉐\n2nd line", 40, "FAILED nodeid::🉐::withunicode - 🉐🉐...") + check("🉐🉐🉐🉐🉐\n2nd line", 41, "FAILED nodeid::🉐::withunicode - 🉐🉐...") + check("🉐🉐🉐🉐🉐\n2nd line", 42, "FAILED nodeid::🉐::withunicode - 🉐🉐🉐...") + check("🉐🉐🉐🉐🉐\n2nd line", 80, "FAILED nodeid::🉐::withunicode - 🉐🉐🉐🉐🉐") @pytest.mark.parametrize( From d742b386c373aeb7ab24e2f5013a537decb97a3a Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Wed, 27 May 2020 00:53:31 -0400 Subject: [PATCH 261/823] provide missing location parameter, and add type annotations to the hookspec --- src/_pytest/hookspec.py | 8 ++++++- src/_pytest/warnings.py | 7 +++++- testing/test_warnings.py | 46 +++++++++++++++++++++++++++++++++------- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index e2810ec78bc..bc8a67ea3d2 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -11,6 +11,7 @@ from _pytest.compat import TYPE_CHECKING if TYPE_CHECKING: + import warnings from _pytest.config import Config from _pytest.main import Session from _pytest.reports import BaseReport @@ -653,7 +654,12 @@ def pytest_warning_captured(warning_message, when, item, location): @hookspec(historic=True) -def pytest_warning_recorded(warning_message, when, nodeid, location): +def pytest_warning_recorded( + warning_message: "warnings.WarningMessage", + when: str, + nodeid: str, + location: Tuple[str, int, str], +): """ Process a warning captured by the internal pytest warnings plugin. diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 3ae3c863929..8828a53d611 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -115,7 +115,12 @@ def catch_warnings_for_item(config, ihook, when, item): kwargs=dict(warning_message=warning_message, when=when, item=item) ) ihook.pytest_warning_recorded.call_historic( - kwargs=dict(warning_message=warning_message, nodeid=nodeid, when=when) + kwargs=dict( + warning_message=warning_message, + nodeid=nodeid, + when=when, + location=None, + ) ) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 6cfdfa6bb81..8b7ef32963a 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -1,4 +1,5 @@ import os +import re import warnings import pytest @@ -268,20 +269,49 @@ def test_func(fix): collected = [] class WarningCollector: - def pytest_warning_recorded(self, warning_message, when, nodeid): - collected.append((str(warning_message.message), when, nodeid)) + def pytest_warning_recorded(self, warning_message, when, nodeid, location): + collected.append((str(warning_message.message), when, nodeid, location)) result = testdir.runpytest(plugins=[WarningCollector()]) result.stdout.fnmatch_lines(["*1 passed*"]) expected = [ - ("config warning", "config", ""), - ("collect warning", "collect", ""), - ("setup warning", "runtest", "test_warning_captured_hook.py::test_func"), - ("call warning", "runtest", "test_warning_captured_hook.py::test_func"), - ("teardown warning", "runtest", "test_warning_captured_hook.py::test_func"), + ( + "config warning", + "config", + "", + ( + r"/tmp/pytest-of-.+/pytest-\d+/test_warning_captured_hook0/conftest.py", + 3, + "pytest_configure", + ), + ), + ("collect warning", "collect", "", None), + ("setup warning", "runtest", "test_warning_captured_hook.py::test_func", None), + ("call warning", "runtest", "test_warning_captured_hook.py::test_func", None), + ( + "teardown warning", + "runtest", + "test_warning_captured_hook.py::test_func", + None, + ), ] - assert collected == expected, str(collected) + for index in range(len(expected)): + collected_result = collected[index] + expected_result = expected[index] + + assert collected_result[0] == expected_result[0], str(collected) + assert collected_result[1] == expected_result[1], str(collected) + assert collected_result[2] == expected_result[2], str(collected) + + if expected_result[3] is not None: + assert re.match(expected_result[3][0], collected_result[3][0]), str( + collected + ) + assert collected_result[3][1] == expected_result[3][1], str(collected) + assert collected_result[3][2] == expected_result[3][2], str(collected) + else: + assert expected_result[3] == collected_result[3], str(collected) @pytest.mark.filterwarnings("always") From 5b9924e1444ee662a58d50fb22eaff23c38d6c03 Mon Sep 17 00:00:00 2001 From: Florian Dahlitz Date: Wed, 27 May 2020 09:27:13 +0200 Subject: [PATCH 262/823] Fix py35 CI run --- testing/test_reports.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/testing/test_reports.py b/testing/test_reports.py index 9e4e7d09de3..81778e27d4b 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -401,9 +401,7 @@ def test_report_prevent_ConftestImportFailure_hiding_exception(self, testdir): sub_dir.join("conftest").new(ext=".py").write("import unknown") result = testdir.runpytest_subprocess(".") - result.stdout.fnmatch_lines( - ["E ModuleNotFoundError: No module named 'unknown'"] - ) + result.stdout.fnmatch_lines(["E *Error: No module named 'unknown'"]) result.stdout.no_fnmatch_line("ERROR - *ConftestImportFailure*") From 97bcf5a3a2fdabfdbc61bee275e97d09be728b08 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 16 Apr 2020 11:29:13 +0300 Subject: [PATCH 263/823] capture: reorder file into sections and avoid forward references Make it easier to read the file in progression, and avoid forward references for upcoming type annotations. There is one cycle, CaptureManager <-> CaptureFixture, which is hard to untangle. (This commit should be added to `.gitblameignore`). --- src/_pytest/capture.py | 1098 ++++++++++++++++++++-------------------- 1 file changed, 561 insertions(+), 537 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 1eb5e1b417f..7a5e854ef2c 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -21,8 +21,6 @@ _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] -patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} - def pytest_addoption(parser): group = parser.getgroup("general") @@ -43,6 +41,105 @@ def pytest_addoption(parser): ) +def _colorama_workaround(): + """ + Ensure colorama is imported so that it attaches to the correct stdio + handles on Windows. + + colorama uses the terminal on import time. So if something does the + first import of colorama while I/O capture is active, colorama will + fail in various ways. + """ + if sys.platform.startswith("win32"): + try: + import colorama # noqa: F401 + except ImportError: + pass + + +def _readline_workaround(): + """ + Ensure readline is imported so that it attaches to the correct stdio + handles on Windows. + + Pdb uses readline support where available--when not running from the Python + prompt, the readline module is not imported until running the pdb REPL. If + running pytest with the --pdb option this means the readline module is not + imported until after I/O capture has been started. + + This is a problem for pyreadline, which is often used to implement readline + support on Windows, as it does not attach to the correct handles for stdout + and/or stdin if they have been redirected by the FDCapture mechanism. This + workaround ensures that readline is imported before I/O capture is setup so + that it can attach to the actual stdin/out for the console. + + See https://github.com/pytest-dev/pytest/pull/1281 + """ + if sys.platform.startswith("win32"): + try: + import readline # noqa: F401 + except ImportError: + pass + + +def _py36_windowsconsoleio_workaround(stream): + """ + Python 3.6 implemented unicode console handling for Windows. This works + by reading/writing to the raw console handle using + ``{Read,Write}ConsoleW``. + + The problem is that we are going to ``dup2`` over the stdio file + descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the + handles used by Python to write to the console. Though there is still some + weirdness and the console handle seems to only be closed randomly and not + on the first call to ``CloseHandle``, or maybe it gets reopened with the + same handle value when we suspend capturing. + + The workaround in this case will reopen stdio with a different fd which + also means a different handle by replicating the logic in + "Py_lifecycle.c:initstdio/create_stdio". + + :param stream: in practice ``sys.stdout`` or ``sys.stderr``, but given + here as parameter for unittesting purposes. + + See https://github.com/pytest-dev/py/issues/103 + """ + if ( + not sys.platform.startswith("win32") + or sys.version_info[:2] < (3, 6) + or hasattr(sys, "pypy_version_info") + ): + return + + # bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666) + if not hasattr(stream, "buffer"): + return + + buffered = hasattr(stream.buffer, "raw") + raw_stdout = stream.buffer.raw if buffered else stream.buffer + + if not isinstance(raw_stdout, io._WindowsConsoleIO): + return + + def _reopen_stdio(f, mode): + if not buffered and mode[0] == "w": + buffering = 0 + else: + buffering = -1 + + return io.TextIOWrapper( + open(os.dup(f.fileno()), mode, buffering), + f.encoding, + f.errors, + f.newlines, + f.line_buffering, + ) + + sys.stdin = _reopen_stdio(sys.stdin, "rb") + sys.stdout = _reopen_stdio(sys.stdout, "wb") + sys.stderr = _reopen_stdio(sys.stderr, "wb") + + @pytest.hookimpl(hookwrapper=True) def pytest_load_initial_conftests(early_config: Config): ns = early_config.known_args_namespace @@ -67,436 +164,160 @@ def pytest_load_initial_conftests(early_config: Config): sys.stderr.write(err) -def _get_multicapture(method: "_CaptureMethod") -> "MultiCapture": - if method == "fd": - return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) - elif method == "sys": - return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2)) - elif method == "no": - return MultiCapture(in_=None, out=None, err=None) - elif method == "tee-sys": - return MultiCapture( - in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True) - ) - raise ValueError("unknown capturing method: {!r}".format(method)) +# IO Helpers. -class CaptureManager: - """ - Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each - test phase (setup, call, teardown). After each of those points, the captured output is obtained and - attached to the collection/runtest report. +class EncodedFile(io.TextIOWrapper): + __slots__ = () - There are two levels of capture: - * global: which is enabled by default and can be suppressed by the ``-s`` option. This is always enabled/disabled - during collection and each test phase. - * fixture: when a test function or one of its fixture depend on the ``capsys`` or ``capfd`` fixtures. In this - case special handling is needed to ensure the fixtures take precedence over the global capture. - """ + @property + def name(self) -> str: + # Ensure that file.name is a string. Workaround for a Python bug + # fixed in >=3.7.4: https://bugs.python.org/issue36015 + return repr(self.buffer) - def __init__(self, method: "_CaptureMethod") -> None: - self._method = method - self._global_capturing = None - self._capture_fixture = None # type: Optional[CaptureFixture] + @property + def mode(self) -> str: + # TextIOWrapper doesn't expose a mode, but at least some of our + # tests check it. + return self.buffer.mode.replace("b", "") - def __repr__(self): - return "".format( - self._method, self._global_capturing, self._capture_fixture - ) - def is_capturing(self): - if self.is_globally_capturing(): - return "global" - if self._capture_fixture: - return "fixture %s" % self._capture_fixture.request.fixturename - return False +class CaptureIO(io.TextIOWrapper): + def __init__(self) -> None: + super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) - # Global capturing control + def getvalue(self) -> str: + assert isinstance(self.buffer, io.BytesIO) + return self.buffer.getvalue().decode("UTF-8") - def is_globally_capturing(self): - return self._method != "no" - def start_global_capturing(self): - assert self._global_capturing is None - self._global_capturing = _get_multicapture(self._method) - self._global_capturing.start_capturing() +class TeeCaptureIO(CaptureIO): + def __init__(self, other: TextIO) -> None: + self._other = other + super().__init__() - def stop_global_capturing(self): - if self._global_capturing is not None: - self._global_capturing.pop_outerr_to_orig() - self._global_capturing.stop_capturing() - self._global_capturing = None + def write(self, s) -> int: + super().write(s) + return self._other.write(s) - def resume_global_capture(self): - # During teardown of the python process, and on rare occasions, capture - # attributes can be `None` while trying to resume global capture. - if self._global_capturing is not None: - self._global_capturing.resume_capturing() - def suspend_global_capture(self, in_=False): - cap = getattr(self, "_global_capturing", None) - if cap is not None: - cap.suspend_capturing(in_=in_) +class DontReadFromInput: + encoding = None - def suspend(self, in_=False): - # Need to undo local capsys-et-al if it exists before disabling global capture. - self.suspend_fixture() - self.suspend_global_capture(in_) + def read(self, *args): + raise OSError( + "pytest: reading from stdin while output is captured! Consider using `-s`." + ) - def resume(self): - self.resume_global_capture() - self.resume_fixture() + readline = read + readlines = read + __next__ = read - def read_global_capture(self): - return self._global_capturing.readouterr() + def __iter__(self): + return self - # Fixture Control + def fileno(self): + raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()") - def set_fixture(self, capture_fixture: "CaptureFixture") -> None: - if self._capture_fixture: - current_fixture = self._capture_fixture.request.fixturename - requested_fixture = capture_fixture.request.fixturename - capture_fixture.request.raiseerror( - "cannot use {} and {} at the same time".format( - requested_fixture, current_fixture - ) - ) - self._capture_fixture = capture_fixture + def isatty(self): + return False - def unset_fixture(self) -> None: - self._capture_fixture = None + def close(self): + pass - def activate_fixture(self): - """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over - the global capture. - """ - if self._capture_fixture: - self._capture_fixture._start() + @property + def buffer(self): + return self - def deactivate_fixture(self): - """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any.""" - if self._capture_fixture: - self._capture_fixture.close() - def suspend_fixture(self): - if self._capture_fixture: - self._capture_fixture._suspend() +# Capture classes. - def resume_fixture(self): - if self._capture_fixture: - self._capture_fixture._resume() - # Helper context managers +patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} - @contextlib.contextmanager - def global_and_fixture_disabled(self): - """Context manager to temporarily disable global and current fixture capturing.""" - self.suspend() - try: - yield - finally: - self.resume() - - @contextlib.contextmanager - def item_capture(self, when, item): - self.resume_global_capture() - self.activate_fixture() - try: - yield - finally: - 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) - - # Hooks - - @pytest.hookimpl(hookwrapper=True) - def pytest_make_collect_report(self, collector): - if isinstance(collector, pytest.File): - self.resume_global_capture() - outcome = yield - 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 - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_setup(self, item): - with self.item_capture("setup", item): - yield - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_call(self, item): - with self.item_capture("call", item): - yield - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_teardown(self, item): - with self.item_capture("teardown", item): - yield - - @pytest.hookimpl(tryfirst=True) - def pytest_keyboard_interrupt(self, excinfo): - self.stop_global_capturing() - - @pytest.hookimpl(tryfirst=True) - def pytest_internalerror(self, excinfo): - self.stop_global_capturing() - - -@pytest.fixture -def capsys(request): - """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. - - The captured output is made available via ``capsys.readouterr()`` method - calls, which return a ``(out, err)`` namedtuple. - ``out`` and ``err`` will be ``text`` objects. - """ - capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture(SysCapture, request) - capman.set_fixture(capture_fixture) - capture_fixture._start() - yield capture_fixture - capture_fixture.close() - capman.unset_fixture() - - -@pytest.fixture -def capsysbinary(request): - """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. - - The captured output is made available via ``capsysbinary.readouterr()`` - method calls, which return a ``(out, err)`` namedtuple. - ``out`` and ``err`` will be ``bytes`` objects. - """ - capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture(SysCaptureBinary, request) - capman.set_fixture(capture_fixture) - capture_fixture._start() - yield capture_fixture - capture_fixture.close() - capman.unset_fixture() - - -@pytest.fixture -def capfd(request): - """Enable text capturing of writes to file descriptors ``1`` and ``2``. - - The captured output is made available via ``capfd.readouterr()`` method - calls, which return a ``(out, err)`` namedtuple. - ``out`` and ``err`` will be ``text`` objects. - """ - capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture(FDCapture, request) - capman.set_fixture(capture_fixture) - capture_fixture._start() - yield capture_fixture - capture_fixture.close() - capman.unset_fixture() - - -@pytest.fixture -def capfdbinary(request): - """Enable bytes capturing of writes to file descriptors ``1`` and ``2``. - - The captured output is made available via ``capfd.readouterr()`` method - calls, which return a ``(out, err)`` namedtuple. - ``out`` and ``err`` will be ``byte`` objects. - """ - capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture(FDCaptureBinary, request) - capman.set_fixture(capture_fixture) - capture_fixture._start() - yield capture_fixture - capture_fixture.close() - capman.unset_fixture() - - -class CaptureIO(io.TextIOWrapper): - def __init__(self) -> None: - super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) - - def getvalue(self) -> str: - assert isinstance(self.buffer, io.BytesIO) - return self.buffer.getvalue().decode("UTF-8") - - -class TeeCaptureIO(CaptureIO): - def __init__(self, other: TextIO) -> None: - self._other = other - super().__init__() - - def write(self, s: str) -> int: - super().write(s) - return self._other.write(s) - - -class CaptureFixture: - """ - Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary` - fixtures. - """ - - def __init__(self, captureclass, request): - self.captureclass = captureclass - self.request = request - self._capture = None - self._captured_out = self.captureclass.EMPTY_BUFFER - self._captured_err = self.captureclass.EMPTY_BUFFER - - def _start(self): - if self._capture is None: - self._capture = MultiCapture( - in_=None, out=self.captureclass(1), err=self.captureclass(2), - ) - self._capture.start_capturing() - - def close(self): - if self._capture is not None: - out, err = self._capture.pop_outerr_to_orig() - self._captured_out += out - self._captured_err += err - self._capture.stop_capturing() - self._capture = None - - def readouterr(self): - """Read and return the captured output so far, resetting the internal buffer. - - :return: captured content as a namedtuple with ``out`` and ``err`` string attributes - """ - captured_out, captured_err = self._captured_out, self._captured_err - if self._capture is not None: - out, err = self._capture.readouterr() - captured_out += out - captured_err += err - self._captured_out = self.captureclass.EMPTY_BUFFER - self._captured_err = self.captureclass.EMPTY_BUFFER - return CaptureResult(captured_out, captured_err) - - def _suspend(self): - """Suspends this fixture's own capturing temporarily.""" - if self._capture is not None: - self._capture.suspend_capturing() - - def _resume(self): - """Resumes this fixture's own capturing temporarily.""" - if self._capture is not None: - self._capture.resume_capturing() - - @contextlib.contextmanager - def disabled(self): - """Temporarily disables capture while inside the 'with' block.""" - capmanager = self.request.config.pluginmanager.getplugin("capturemanager") - with capmanager.global_and_fixture_disabled(): - yield - - -class EncodedFile(io.TextIOWrapper): - __slots__ = () - - @property - def name(self) -> str: - # Ensure that file.name is a string. Workaround for a Python bug - # fixed in >=3.7.4: https://bugs.python.org/issue36015 - return repr(self.buffer) - - @property - def mode(self) -> str: - # TextIOWrapper doesn't expose a mode, but at least some of our - # tests check it. - return self.buffer.mode.replace("b", "") +class NoCapture: + EMPTY_BUFFER = None + __init__ = start = done = suspend = resume = lambda *args: None -CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"]) +class SysCaptureBinary: -class MultiCapture: + EMPTY_BUFFER = b"" _state = None - _in_suspended = False - def __init__(self, in_, out, err) -> None: - self.in_ = in_ - self.out = out - self.err = err + def __init__(self, fd, tmpfile=None, *, tee=False): + name = patchsysdict[fd] + self._old = getattr(sys, name) + self.name = name + if tmpfile is None: + if name == "stdin": + tmpfile = DontReadFromInput() + else: + tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old) + self.tmpfile = tmpfile - def __repr__(self): - return "".format( - self.out, self.err, self.in_, self._state, self._in_suspended, + def repr(self, class_name: str) -> str: + return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( + class_name, + self.name, + hasattr(self, "_old") and repr(self._old) or "", + self._state, + self.tmpfile, ) - def start_capturing(self): + def __repr__(self) -> str: + return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( + self.__class__.__name__, + self.name, + hasattr(self, "_old") and repr(self._old) or "", + self._state, + self.tmpfile, + ) + + def start(self): + setattr(sys, self.name, self.tmpfile) self._state = "started" - if self.in_: - self.in_.start() - if self.out: - self.out.start() - if self.err: - self.err.start() - def pop_outerr_to_orig(self): - """ pop current snapshot out/err capture and flush to orig streams. """ - out, err = self.readouterr() - if out: - self.out.writeorg(out) - if err: - self.err.writeorg(err) - return out, err + def snap(self): + res = self.tmpfile.buffer.getvalue() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res - def suspend_capturing(self, in_=False): + def done(self): + setattr(sys, self.name, self._old) + del self._old + self.tmpfile.close() + self._state = "done" + + def suspend(self): + setattr(sys, self.name, self._old) self._state = "suspended" - if self.out: - self.out.suspend() - if self.err: - self.err.suspend() - if in_ and self.in_: - self.in_.suspend() - self._in_suspended = True - def resume_capturing(self): + def resume(self): + setattr(sys, self.name, self.tmpfile) self._state = "resumed" - if self.out: - self.out.resume() - if self.err: - self.err.resume() - if self._in_suspended: - self.in_.resume() - self._in_suspended = False - def stop_capturing(self): - """ stop capturing and reset capturing streams """ - if self._state == "stopped": - raise ValueError("was already stopped") - self._state = "stopped" - if self.out: - self.out.done() - if self.err: - self.err.done() - if self.in_: - self.in_.done() - - def readouterr(self) -> CaptureResult: - if self.out: - out = self.out.snap() - else: - out = "" - if self.err: - err = self.err.snap() - else: - err = "" - return CaptureResult(out, err) + def writeorg(self, data): + self._old.flush() + self._old.buffer.write(data) + self._old.buffer.flush() -class NoCapture: - EMPTY_BUFFER = None - __init__ = start = done = suspend = resume = lambda *args: None +class SysCapture(SysCaptureBinary): + EMPTY_BUFFER = "" # type: ignore[assignment] + + def snap(self): + res = self.tmpfile.getvalue() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + def writeorg(self, data): + self._old.write(data) + self._old.flush() class FDCaptureBinary: @@ -617,198 +438,401 @@ def writeorg(self, data): super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream -class SysCaptureBinary: +# MultiCapture - EMPTY_BUFFER = b"" +CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"]) + + +class MultiCapture: _state = None + _in_suspended = False - def __init__(self, fd, tmpfile=None, *, tee=False): - name = patchsysdict[fd] - self._old = getattr(sys, name) - self.name = name - if tmpfile is None: - if name == "stdin": - tmpfile = DontReadFromInput() - else: - tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old) - self.tmpfile = tmpfile + def __init__(self, in_, out, err) -> None: + self.in_ = in_ + self.out = out + self.err = err def __repr__(self): - return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( - self.__class__.__name__, - self.name, - hasattr(self, "_old") and repr(self._old) or "", - self._state, - self.tmpfile, + return "".format( + self.out, self.err, self.in_, self._state, self._in_suspended, ) - def start(self): - setattr(sys, self.name, self.tmpfile) + def start_capturing(self): self._state = "started" + if self.in_: + self.in_.start() + if self.out: + self.out.start() + if self.err: + self.err.start() - def snap(self): - res = self.tmpfile.buffer.getvalue() - self.tmpfile.seek(0) - self.tmpfile.truncate() - return res - - def done(self): - setattr(sys, self.name, self._old) - del self._old - self.tmpfile.close() - self._state = "done" + def pop_outerr_to_orig(self): + """ pop current snapshot out/err capture and flush to orig streams. """ + out, err = self.readouterr() + if out: + self.out.writeorg(out) + if err: + self.err.writeorg(err) + return out, err - def suspend(self): - setattr(sys, self.name, self._old) + def suspend_capturing(self, in_=False): self._state = "suspended" + if self.out: + self.out.suspend() + if self.err: + self.err.suspend() + if in_ and self.in_: + self.in_.suspend() + self._in_suspended = True - def resume(self): - setattr(sys, self.name, self.tmpfile) + def resume_capturing(self): self._state = "resumed" + if self.out: + self.out.resume() + if self.err: + self.err.resume() + if self._in_suspended: + self.in_.resume() + self._in_suspended = False + + def stop_capturing(self): + """ stop capturing and reset capturing streams """ + if self._state == "stopped": + raise ValueError("was already stopped") + self._state = "stopped" + if self.out: + self.out.done() + if self.err: + self.err.done() + if self.in_: + self.in_.done() + + def readouterr(self) -> CaptureResult: + if self.out: + out = self.out.snap() + else: + out = "" + if self.err: + err = self.err.snap() + else: + err = "" + return CaptureResult(out, err) + + +def _get_multicapture(method: "_CaptureMethod") -> MultiCapture: + if method == "fd": + return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) + elif method == "sys": + return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2)) + elif method == "no": + return MultiCapture(in_=None, out=None, err=None) + elif method == "tee-sys": + return MultiCapture( + in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True) + ) + raise ValueError("unknown capturing method: {!r}".format(method)) + + +# CaptureManager and CaptureFixture + + +class CaptureManager: + """ + Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each + test phase (setup, call, teardown). After each of those points, the captured output is obtained and + attached to the collection/runtest report. + + There are two levels of capture: + * global: which is enabled by default and can be suppressed by the ``-s`` option. This is always enabled/disabled + during collection and each test phase. + * fixture: when a test function or one of its fixture depend on the ``capsys`` or ``capfd`` fixtures. In this + case special handling is needed to ensure the fixtures take precedence over the global capture. + """ + + def __init__(self, method: "_CaptureMethod") -> None: + self._method = method + self._global_capturing = None + self._capture_fixture = None # type: Optional[CaptureFixture] + + def __repr__(self): + return "".format( + self._method, self._global_capturing, self._capture_fixture + ) + + def is_capturing(self): + if self.is_globally_capturing(): + return "global" + if self._capture_fixture: + return "fixture %s" % self._capture_fixture.request.fixturename + return False + + # Global capturing control + + def is_globally_capturing(self): + return self._method != "no" + + def start_global_capturing(self): + assert self._global_capturing is None + self._global_capturing = _get_multicapture(self._method) + self._global_capturing.start_capturing() + + def stop_global_capturing(self): + if self._global_capturing is not None: + self._global_capturing.pop_outerr_to_orig() + self._global_capturing.stop_capturing() + self._global_capturing = None + + def resume_global_capture(self): + # During teardown of the python process, and on rare occasions, capture + # attributes can be `None` while trying to resume global capture. + if self._global_capturing is not None: + self._global_capturing.resume_capturing() + + def suspend_global_capture(self, in_=False): + cap = getattr(self, "_global_capturing", None) + if cap is not None: + cap.suspend_capturing(in_=in_) + + def suspend(self, in_=False): + # Need to undo local capsys-et-al if it exists before disabling global capture. + self.suspend_fixture() + self.suspend_global_capture(in_) + + def resume(self): + self.resume_global_capture() + self.resume_fixture() + + def read_global_capture(self): + return self._global_capturing.readouterr() + + # Fixture Control + + def set_fixture(self, capture_fixture: "CaptureFixture") -> None: + if self._capture_fixture: + current_fixture = self._capture_fixture.request.fixturename + requested_fixture = capture_fixture.request.fixturename + capture_fixture.request.raiseerror( + "cannot use {} and {} at the same time".format( + requested_fixture, current_fixture + ) + ) + self._capture_fixture = capture_fixture + + def unset_fixture(self) -> None: + self._capture_fixture = None + + def activate_fixture(self): + """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over + the global capture. + """ + if self._capture_fixture: + self._capture_fixture._start() + + def deactivate_fixture(self): + """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any.""" + if self._capture_fixture: + self._capture_fixture.close() + + def suspend_fixture(self): + if self._capture_fixture: + self._capture_fixture._suspend() + + def resume_fixture(self): + if self._capture_fixture: + self._capture_fixture._resume() + + # Helper context managers + + @contextlib.contextmanager + def global_and_fixture_disabled(self): + """Context manager to temporarily disable global and current fixture capturing.""" + self.suspend() + try: + yield + finally: + self.resume() + + @contextlib.contextmanager + def item_capture(self, when, item): + self.resume_global_capture() + self.activate_fixture() + try: + yield + finally: + self.deactivate_fixture() + self.suspend_global_capture(in_=False) - def writeorg(self, data): - self._old.flush() - self._old.buffer.write(data) - self._old.buffer.flush() + out, err = self.read_global_capture() + item.add_report_section(when, "stdout", out) + item.add_report_section(when, "stderr", err) + # Hooks -class SysCapture(SysCaptureBinary): - EMPTY_BUFFER = "" # type: ignore[assignment] + @pytest.hookimpl(hookwrapper=True) + def pytest_make_collect_report(self, collector): + if isinstance(collector, pytest.File): + self.resume_global_capture() + outcome = yield + 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 - def snap(self): - res = self.tmpfile.getvalue() - self.tmpfile.seek(0) - self.tmpfile.truncate() - return res + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_setup(self, item): + with self.item_capture("setup", item): + yield - def writeorg(self, data): - self._old.write(data) - self._old.flush() + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_call(self, item): + with self.item_capture("call", item): + yield + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_teardown(self, item): + with self.item_capture("teardown", item): + yield -class DontReadFromInput: - encoding = None + @pytest.hookimpl(tryfirst=True) + def pytest_keyboard_interrupt(self, excinfo): + self.stop_global_capturing() - def read(self, *args): - raise OSError( - "pytest: reading from stdin while output is captured! Consider using `-s`." - ) + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(self, excinfo): + self.stop_global_capturing() - readline = read - readlines = read - __next__ = read - def __iter__(self): - return self +class CaptureFixture: + """ + Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary` + fixtures. + """ - def fileno(self): - raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()") + def __init__(self, captureclass, request): + self.captureclass = captureclass + self.request = request + self._capture = None + self._captured_out = self.captureclass.EMPTY_BUFFER + self._captured_err = self.captureclass.EMPTY_BUFFER - def isatty(self): - return False + def _start(self): + if self._capture is None: + self._capture = MultiCapture( + in_=None, out=self.captureclass(1), err=self.captureclass(2), + ) + self._capture.start_capturing() def close(self): - pass - - @property - def buffer(self): - return self + if self._capture is not None: + out, err = self._capture.pop_outerr_to_orig() + self._captured_out += out + self._captured_err += err + self._capture.stop_capturing() + self._capture = None + def readouterr(self): + """Read and return the captured output so far, resetting the internal buffer. -def _colorama_workaround(): - """ - Ensure colorama is imported so that it attaches to the correct stdio - handles on Windows. + :return: captured content as a namedtuple with ``out`` and ``err`` string attributes + """ + captured_out, captured_err = self._captured_out, self._captured_err + if self._capture is not None: + out, err = self._capture.readouterr() + captured_out += out + captured_err += err + self._captured_out = self.captureclass.EMPTY_BUFFER + self._captured_err = self.captureclass.EMPTY_BUFFER + return CaptureResult(captured_out, captured_err) - colorama uses the terminal on import time. So if something does the - first import of colorama while I/O capture is active, colorama will - fail in various ways. - """ - if sys.platform.startswith("win32"): - try: - import colorama # noqa: F401 - except ImportError: - pass + def _suspend(self): + """Suspends this fixture's own capturing temporarily.""" + if self._capture is not None: + self._capture.suspend_capturing() + def _resume(self): + """Resumes this fixture's own capturing temporarily.""" + if self._capture is not None: + self._capture.resume_capturing() -def _readline_workaround(): - """ - Ensure readline is imported so that it attaches to the correct stdio - handles on Windows. + @contextlib.contextmanager + def disabled(self): + """Temporarily disables capture while inside the 'with' block.""" + capmanager = self.request.config.pluginmanager.getplugin("capturemanager") + with capmanager.global_and_fixture_disabled(): + yield - Pdb uses readline support where available--when not running from the Python - prompt, the readline module is not imported until running the pdb REPL. If - running pytest with the --pdb option this means the readline module is not - imported until after I/O capture has been started. - This is a problem for pyreadline, which is often used to implement readline - support on Windows, as it does not attach to the correct handles for stdout - and/or stdin if they have been redirected by the FDCapture mechanism. This - workaround ensures that readline is imported before I/O capture is setup so - that it can attach to the actual stdin/out for the console. +# The fixtures. - See https://github.com/pytest-dev/pytest/pull/1281 - """ - if sys.platform.startswith("win32"): - try: - import readline # noqa: F401 - except ImportError: - pass +@pytest.fixture +def capsys(request): + """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. -def _py36_windowsconsoleio_workaround(stream): + The captured output is made available via ``capsys.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. """ - Python 3.6 implemented unicode console handling for Windows. This works - by reading/writing to the raw console handle using - ``{Read,Write}ConsoleW``. - - The problem is that we are going to ``dup2`` over the stdio file - descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the - handles used by Python to write to the console. Though there is still some - weirdness and the console handle seems to only be closed randomly and not - on the first call to ``CloseHandle``, or maybe it gets reopened with the - same handle value when we suspend capturing. + capman = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(SysCapture, request) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() - The workaround in this case will reopen stdio with a different fd which - also means a different handle by replicating the logic in - "Py_lifecycle.c:initstdio/create_stdio". - :param stream: in practice ``sys.stdout`` or ``sys.stderr``, but given - here as parameter for unittesting purposes. +@pytest.fixture +def capsysbinary(request): + """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. - See https://github.com/pytest-dev/py/issues/103 + The captured output is made available via ``capsysbinary.readouterr()`` + method calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``bytes`` objects. """ - if ( - not sys.platform.startswith("win32") - or sys.version_info[:2] < (3, 6) - or hasattr(sys, "pypy_version_info") - ): - return + capman = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(SysCaptureBinary, request) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() - # bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666) - if not hasattr(stream, "buffer"): - return - buffered = hasattr(stream.buffer, "raw") - raw_stdout = stream.buffer.raw if buffered else stream.buffer +@pytest.fixture +def capfd(request): + """Enable text capturing of writes to file descriptors ``1`` and ``2``. - if not isinstance(raw_stdout, io._WindowsConsoleIO): - return + The captured output is made available via ``capfd.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. + """ + capman = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(FDCapture, request) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() - def _reopen_stdio(f, mode): - if not buffered and mode[0] == "w": - buffering = 0 - else: - buffering = -1 - return io.TextIOWrapper( - open(os.dup(f.fileno()), mode, buffering), - f.encoding, - f.errors, - f.newlines, - f.line_buffering, - ) +@pytest.fixture +def capfdbinary(request): + """Enable bytes capturing of writes to file descriptors ``1`` and ``2``. - sys.stdin = _reopen_stdio(sys.stdin, "rb") - sys.stdout = _reopen_stdio(sys.stdout, "wb") - sys.stderr = _reopen_stdio(sys.stderr, "wb") + The captured output is made available via ``capfd.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``byte`` objects. + """ + capman = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(FDCaptureBinary, request) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() From fd3ba053cfab5d376b66a880f41834638e93fa0f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 16 Apr 2020 20:59:27 +0300 Subject: [PATCH 264/823] capture: don't assume that the tmpfile is backed by a BytesIO Since tmpfile is a parameter to SysCapture, it shouldn't assume things unnecessarily, when there is an alternative. --- src/_pytest/capture.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 7a5e854ef2c..a078925638c 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -281,7 +281,8 @@ def start(self): self._state = "started" def snap(self): - res = self.tmpfile.buffer.getvalue() + self.tmpfile.seek(0) + res = self.tmpfile.buffer.read() self.tmpfile.seek(0) self.tmpfile.truncate() return res From a35800c2e1461d32dfd4322dc21d02e46c49c776 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 17 Apr 2020 10:01:51 +0300 Subject: [PATCH 265/823] capture: formalize and check allowed state transition in capture classes There are state transitions start/done/suspend/resume and two additional operations snap/writeorg. Previously it was not well defined in what order they can be called, and which operations are idempotent. Formalize this and enforce using assert checks with informative error messages if they fail (rather than random AttributeErrors). --- src/_pytest/capture.py | 48 +++++++++++++++++++++++++++++++++++++---- testing/test_capture.py | 14 ++++++------ 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index a078925638c..32e83dd214f 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -11,6 +11,7 @@ from tempfile import TemporaryFile from typing import Optional from typing import TextIO +from typing import Tuple import pytest from _pytest.compat import TYPE_CHECKING @@ -245,7 +246,6 @@ class NoCapture: class SysCaptureBinary: EMPTY_BUFFER = b"" - _state = None def __init__(self, fd, tmpfile=None, *, tee=False): name = patchsysdict[fd] @@ -257,6 +257,7 @@ def __init__(self, fd, tmpfile=None, *, tee=False): else: tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old) self.tmpfile = tmpfile + self._state = "initialized" def repr(self, class_name: str) -> str: return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( @@ -276,11 +277,20 @@ def __repr__(self) -> str: self.tmpfile, ) + def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: + assert ( + self._state in states + ), "cannot {} in state {!r}: expected one of {}".format( + op, self._state, ", ".join(states) + ) + def start(self): + self._assert_state("start", ("initialized",)) setattr(sys, self.name, self.tmpfile) self._state = "started" def snap(self): + self._assert_state("snap", ("started", "suspended")) self.tmpfile.seek(0) res = self.tmpfile.buffer.read() self.tmpfile.seek(0) @@ -288,20 +298,28 @@ def snap(self): return res def done(self): + self._assert_state("done", ("initialized", "started", "suspended", "done")) + if self._state == "done": + return setattr(sys, self.name, self._old) del self._old self.tmpfile.close() self._state = "done" def suspend(self): + self._assert_state("suspend", ("started", "suspended")) setattr(sys, self.name, self._old) self._state = "suspended" def resume(self): + self._assert_state("resume", ("started", "suspended")) + if self._state == "started": + return setattr(sys, self.name, self.tmpfile) - self._state = "resumed" + self._state = "started" def writeorg(self, data): + self._assert_state("writeorg", ("started", "suspended")) self._old.flush() self._old.buffer.write(data) self._old.buffer.flush() @@ -317,6 +335,7 @@ def snap(self): return res def writeorg(self, data): + self._assert_state("writeorg", ("started", "suspended")) self._old.write(data) self._old.flush() @@ -328,7 +347,6 @@ class FDCaptureBinary: """ EMPTY_BUFFER = b"" - _state = None def __init__(self, targetfd): self.targetfd = targetfd @@ -368,6 +386,8 @@ def __init__(self, targetfd): else: self.syscapture = NoCapture() + self._state = "initialized" + def __repr__(self): return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format( self.__class__.__name__, @@ -377,13 +397,22 @@ def __repr__(self): self.tmpfile, ) + def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: + assert ( + self._state in states + ), "cannot {} in state {!r}: expected one of {}".format( + op, self._state, ", ".join(states) + ) + def start(self): """ Start capturing on targetfd using memorized tmpfile. """ + self._assert_state("start", ("initialized",)) os.dup2(self.tmpfile.fileno(), self.targetfd) self.syscapture.start() self._state = "started" def snap(self): + self._assert_state("snap", ("started", "suspended")) self.tmpfile.seek(0) res = self.tmpfile.buffer.read() self.tmpfile.seek(0) @@ -393,6 +422,9 @@ def snap(self): def done(self): """ stop capturing, restore streams, return original capture file, seeked to position zero. """ + self._assert_state("done", ("initialized", "started", "suspended", "done")) + if self._state == "done": + return os.dup2(self.targetfd_save, self.targetfd) os.close(self.targetfd_save) if self.targetfd_invalid is not None: @@ -404,17 +436,24 @@ def done(self): self._state = "done" def suspend(self): + self._assert_state("suspend", ("started", "suspended")) + if self._state == "suspended": + return self.syscapture.suspend() os.dup2(self.targetfd_save, self.targetfd) self._state = "suspended" def resume(self): + self._assert_state("resume", ("started", "suspended")) + if self._state == "started": + return self.syscapture.resume() os.dup2(self.tmpfile.fileno(), self.targetfd) - self._state = "resumed" + self._state = "started" def writeorg(self, data): """ write to original file descriptor. """ + self._assert_state("writeorg", ("started", "suspended")) os.write(self.targetfd_save, data) @@ -428,6 +467,7 @@ class FDCapture(FDCaptureBinary): EMPTY_BUFFER = "" # type: ignore def snap(self): + self._assert_state("snap", ("started", "suspended")) self.tmpfile.seek(0) res = self.tmpfile.read() self.tmpfile.seek(0) diff --git a/testing/test_capture.py b/testing/test_capture.py index 5a0998da7ae..95f2d748a32 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -878,9 +878,8 @@ def test_simple(self, tmpfile): cap = capture.FDCapture(fd) data = b"hello" os.write(fd, data) - s = cap.snap() + pytest.raises(AssertionError, cap.snap) cap.done() - assert not s cap = capture.FDCapture(fd) cap.start() os.write(fd, data) @@ -901,7 +900,7 @@ def test_simple_fail_second_start(self, tmpfile): fd = tmpfile.fileno() cap = capture.FDCapture(fd) cap.done() - pytest.raises(ValueError, cap.start) + pytest.raises(AssertionError, cap.start) def test_stderr(self): cap = capture.FDCapture(2) @@ -952,7 +951,7 @@ def test_simple_resume_suspend(self): assert s == "but now yes\n" cap.suspend() cap.done() - pytest.raises(AttributeError, cap.suspend) + pytest.raises(AssertionError, cap.suspend) assert repr(cap) == ( "".format( @@ -1154,6 +1153,7 @@ def test_many(self, capfd): with lsof_check(): for i in range(10): cap = StdCaptureFD() + cap.start_capturing() cap.stop_capturing() @@ -1175,7 +1175,7 @@ def StdCaptureFD(out=True, err=True, in_=True): def test_stdout(): os.close(1) cap = StdCaptureFD(out=True, err=False, in_=False) - assert fnmatch(repr(cap.out), "") + assert fnmatch(repr(cap.out), "") cap.start_capturing() os.write(1, b"stdout") assert cap.readouterr() == ("stdout", "") @@ -1184,7 +1184,7 @@ def test_stdout(): def test_stderr(): os.close(2) cap = StdCaptureFD(out=False, err=True, in_=False) - assert fnmatch(repr(cap.err), "") + assert fnmatch(repr(cap.err), "") cap.start_capturing() os.write(2, b"stderr") assert cap.readouterr() == ("", "stderr") @@ -1193,7 +1193,7 @@ def test_stderr(): def test_stdin(): os.close(0) cap = StdCaptureFD(out=False, err=False, in_=True) - assert fnmatch(repr(cap.in_), "") + assert fnmatch(repr(cap.in_), "") cap.stop_capturing() """ ) From 7a704288df8ed2d416c6dd8b15df3664ad425a5a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 17 Apr 2020 22:52:09 +0300 Subject: [PATCH 266/823] capture: remove unneeded getattr This attribute is set in __init__ and not deleted. Other methods do it already but this one wasn't updated. --- src/_pytest/capture.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 32e83dd214f..64f4b8b92f1 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -630,9 +630,8 @@ def resume_global_capture(self): self._global_capturing.resume_capturing() def suspend_global_capture(self, in_=False): - cap = getattr(self, "_global_capturing", None) - if cap is not None: - cap.suspend_capturing(in_=in_) + if self._global_capturing is not None: + self._global_capturing.suspend_capturing(in_=in_) def suspend(self, in_=False): # Need to undo local capsys-et-al if it exists before disabling global capture. From f93e021bc87c17528e0c1aaebceea178b22b7470 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 27 May 2020 13:04:56 +0300 Subject: [PATCH 267/823] capture: remove some unclear parametrization from a test The two cases end up doing the same (the tmpfile fixture isn't used except being truthy). --- testing/test_capture.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index 95f2d748a32..1301a0e69d3 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1255,11 +1255,8 @@ def test_capsys_results_accessible_by_attribute(capsys): assert capture_result.err == "eggs" -@pytest.mark.parametrize("use", [True, False]) -def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): - if not use: - tmpfile = True - cap = StdCaptureFD(out=False, err=tmpfile) +def test_fdcapture_tmpfile_remains_the_same() -> None: + cap = StdCaptureFD(out=False, err=True) try: cap.start_capturing() capfile = cap.err.tmpfile From 14de08011b05bffe66df6198ac4ccbac5c8aa7fe Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Wed, 27 May 2020 23:03:07 -0400 Subject: [PATCH 268/823] fix the unit tests, add the proper deprecation warning, and add in a changelog entry --- changelog/7255.feature.rst | 3 +++ src/_pytest/deprecated.py | 5 +++++ src/_pytest/hookspec.py | 8 ++------ testing/test_warnings.py | 40 +++++++++++++------------------------- 4 files changed, 23 insertions(+), 33 deletions(-) create mode 100644 changelog/7255.feature.rst diff --git a/changelog/7255.feature.rst b/changelog/7255.feature.rst new file mode 100644 index 00000000000..4073589b05a --- /dev/null +++ b/changelog/7255.feature.rst @@ -0,0 +1,3 @@ +Introduced a new hook named `pytest_warning_recorded` to convey information about warnings captured by the internal `pytest` warnings plugin. + +This hook is meant to replace `pytest_warning_captured`, which will be removed in a future release. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index f981a4a4b9e..1ce4e1e39b2 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -80,3 +80,8 @@ "The `-k 'expr:'` syntax to -k is deprecated.\n" "Please open an issue if you use this and want a replacement." ) + +WARNING_CAPTURED_HOOK = PytestDeprecationWarning( + "The pytest_warning_captured is deprecated and will be removed in a future release.\n" + "Please use pytest_warning_recorded instead." +) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index bc8a67ea3d2..341f0a250b2 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -8,6 +8,7 @@ from pluggy import HookspecMarker from .deprecated import COLLECT_DIRECTORY_HOOK +from .deprecated import WARNING_CAPTURED_HOOK from _pytest.compat import TYPE_CHECKING if TYPE_CHECKING: @@ -621,12 +622,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): """ -@hookspec( - historic=True, - warn_on_impl=DeprecationWarning( - "pytest_warning_captured is deprecated and will be removed soon" - ), -) +@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK) def pytest_warning_captured(warning_message, when, item, location): """(**Deprecated**) Process a warning captured by the internal pytest warnings plugin. diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 8b7ef32963a..070ed72c51f 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -1,5 +1,4 @@ import os -import re import warnings import pytest @@ -276,25 +275,11 @@ def pytest_warning_recorded(self, warning_message, when, nodeid, location): result.stdout.fnmatch_lines(["*1 passed*"]) expected = [ - ( - "config warning", - "config", - "", - ( - r"/tmp/pytest-of-.+/pytest-\d+/test_warning_captured_hook0/conftest.py", - 3, - "pytest_configure", - ), - ), - ("collect warning", "collect", "", None), - ("setup warning", "runtest", "test_warning_captured_hook.py::test_func", None), - ("call warning", "runtest", "test_warning_captured_hook.py::test_func", None), - ( - "teardown warning", - "runtest", - "test_warning_captured_hook.py::test_func", - None, - ), + ("config warning", "config", "",), + ("collect warning", "collect", ""), + ("setup warning", "runtest", "test_warning_captured_hook.py::test_func"), + ("call warning", "runtest", "test_warning_captured_hook.py::test_func"), + ("teardown warning", "runtest", "test_warning_captured_hook.py::test_func"), ] for index in range(len(expected)): collected_result = collected[index] @@ -304,14 +289,15 @@ def pytest_warning_recorded(self, warning_message, when, nodeid, location): assert collected_result[1] == expected_result[1], str(collected) assert collected_result[2] == expected_result[2], str(collected) - if expected_result[3] is not None: - assert re.match(expected_result[3][0], collected_result[3][0]), str( - collected - ) - assert collected_result[3][1] == expected_result[3][1], str(collected) - assert collected_result[3][2] == expected_result[3][2], str(collected) + # NOTE: collected_result[3] is location, which differs based on the platform you are on + # thus, the best we can do here is assert the types of the paremeters match what we expect + # and not try and preload it in the expected array + if collected_result[3] is not None: + assert type(collected_result[3][0]) is str, str(collected) + assert type(collected_result[3][1]) is int, str(collected) + assert type(collected_result[3][2]) is str, str(collected) else: - assert expected_result[3] == collected_result[3], str(collected) + assert collected_result[3] is None, str(collected) @pytest.mark.filterwarnings("always") From 2af0d1e221739ebad1d318743d2987f3d45511f8 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Thu, 28 May 2020 00:02:28 -0400 Subject: [PATCH 269/823] remove a stray comma in a test tuple --- testing/test_warnings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 070ed72c51f..ea7ab397dfe 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -275,7 +275,7 @@ def pytest_warning_recorded(self, warning_message, when, nodeid, location): result.stdout.fnmatch_lines(["*1 passed*"]) expected = [ - ("config warning", "config", "",), + ("config warning", "config", ""), ("collect warning", "collect", ""), ("setup warning", "runtest", "test_warning_captured_hook.py::test_func"), ("call warning", "runtest", "test_warning_captured_hook.py::test_func"), From 2ee90887b77212e2e8f427ed6db9feab85f06b49 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 28 May 2020 12:12:10 +0300 Subject: [PATCH 270/823] code: remove last usage of py.error `str(self.path)` can't raise at all, so it can just be removed. --- src/_pytest/_code/code.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 2075fd0eb34..7e8cff2eda4 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -268,10 +268,6 @@ def ishidden(self): return tbh def __str__(self) -> str: - try: - fn = str(self.path) - except py.error.Error: - fn = "???" name = self.frame.code.name try: line = str(self.statement).lstrip() @@ -279,7 +275,7 @@ def __str__(self) -> str: raise except BaseException: line = "???" - return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line) + return " File %r:%d in %s\n %s\n" % (self.path, self.lineno + 1, name, line) @property def name(self) -> str: From 94c7b8b47cd6b5b14f463731e473929b42881073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Katarzyna=20Kr=C3=B3l?= <38542683+CarycaKatarzyna@users.noreply.github.com> Date: Sat, 30 May 2020 13:10:58 +0200 Subject: [PATCH 271/823] Issue 1316 - longrepr is a string when pytrace=False (#7100) --- changelog/1316.breaking.rst | 1 + src/_pytest/_code/code.py | 44 ++++++++++++++++++++++++------------- src/_pytest/junitxml.py | 4 +--- src/_pytest/nodes.py | 2 +- testing/test_runner.py | 11 ++++++++++ testing/test_skipping.py | 2 +- 6 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 changelog/1316.breaking.rst diff --git a/changelog/1316.breaking.rst b/changelog/1316.breaking.rst new file mode 100644 index 00000000000..4c01de728e8 --- /dev/null +++ b/changelog/1316.breaking.rst @@ -0,0 +1 @@ +``TestReport.longrepr`` is now always an instance of ``ReprExceptionInfo``. Previously it was a ``str`` when a test failed with ``pytest.fail(..., pytrace=False)``. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 7e8cff2eda4..7b17d761274 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -46,7 +46,7 @@ from typing_extensions import Literal from weakref import ReferenceType - _TracebackStyle = Literal["long", "short", "line", "no", "native"] + _TracebackStyle = Literal["long", "short", "line", "no", "native", "value"] class Code: @@ -583,7 +583,7 @@ def getrepr( Show locals per traceback entry. Ignored if ``style=="native"``. - :param str style: long|short|no|native traceback style + :param str style: long|short|no|native|value traceback style :param bool abspath: If paths should be changed to absolute or left unchanged. @@ -758,16 +758,15 @@ def repr_locals(self, locals: Dict[str, object]) -> Optional["ReprLocals"]: def repr_traceback_entry( self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None ) -> "ReprEntry": - source = self._getentrysource(entry) - if source is None: - source = Source("???") - line_index = 0 - else: - line_index = entry.lineno - entry.getfirstlinesource() - lines = [] # type: List[str] style = entry._repr_style if entry._repr_style is not None else self.style if style in ("short", "long"): + source = self._getentrysource(entry) + if source is None: + source = Source("???") + line_index = 0 + else: + line_index = entry.lineno - entry.getfirstlinesource() short = style == "short" reprargs = self.repr_args(entry) if not short else None s = self.get_source(source, line_index, excinfo, short=short) @@ -780,9 +779,14 @@ def repr_traceback_entry( reprfileloc = ReprFileLocation(path, entry.lineno + 1, message) localsrepr = self.repr_locals(entry.locals) return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style) - if excinfo: - lines.extend(self.get_exconly(excinfo, indent=4)) - return ReprEntry(lines, None, None, None, style) + elif style == "value": + if excinfo: + lines.extend(str(excinfo.value).split("\n")) + return ReprEntry(lines, None, None, None, style) + else: + if excinfo: + lines.extend(self.get_exconly(excinfo, indent=4)) + return ReprEntry(lines, None, None, None, style) def _makepath(self, path): if not self.abspath: @@ -806,6 +810,11 @@ def repr_traceback(self, excinfo: ExceptionInfo) -> "ReprTraceback": last = traceback[-1] entries = [] + if self.style == "value": + reprentry = self.repr_traceback_entry(last, excinfo) + entries.append(reprentry) + return ReprTraceback(entries, None, style=self.style) + for index, entry in enumerate(traceback): einfo = (last == entry) and excinfo or None reprentry = self.repr_traceback_entry(entry, einfo) @@ -865,7 +874,9 @@ def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr": seen.add(id(e)) if excinfo_: reprtraceback = self.repr_traceback(excinfo_) - reprcrash = excinfo_._getreprcrash() # type: Optional[ReprFileLocation] + reprcrash = ( + excinfo_._getreprcrash() if self.style != "value" else None + ) # type: Optional[ReprFileLocation] else: # fallback to native repr if the exception doesn't have a traceback: # ExceptionInfo objects require a full traceback to work @@ -1048,8 +1059,11 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None: "Unexpected failure lines between source lines:\n" + "\n".join(self.lines) ) - indents.append(line[:indent_size]) - source_lines.append(line[indent_size:]) + if self.style == "value": + source_lines.append(line) + else: + indents.append(line[:indent_size]) + source_lines.append(line[indent_size:]) else: seeing_failures = True failure_lines.append(line) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 77e1843127a..4a1afc63e1f 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -202,10 +202,8 @@ def append_failure(self, report): if hasattr(report, "wasxfail"): self._add_simple(Junit.skipped, "xfail-marked test passes unexpectedly") else: - if hasattr(report.longrepr, "reprcrash"): + if getattr(report.longrepr, "reprcrash", None) is not None: message = report.longrepr.reprcrash.message - elif isinstance(report.longrepr, str): - message = report.longrepr else: message = str(report.longrepr) message = bin_xml_escape(message) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 2ed25061063..4a79bc86157 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -337,7 +337,7 @@ def _repr_failure_py( excinfo = ExceptionInfo(excinfo.value.excinfo) if isinstance(excinfo.value, fail.Exception): if not excinfo.value.pytrace: - return str(excinfo.value) + style = "value" if isinstance(excinfo.value, FixtureLookupError): return excinfo.value.formatrepr() if self.config.getoption("fulltrace", False): diff --git a/testing/test_runner.py b/testing/test_runner.py index 00732d03b26..7b0b27a4b0b 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -1002,6 +1002,17 @@ def test_func(): assert rep.capstdout == "" assert rep.capstderr == "" + def test_longrepr_type(self, testdir) -> None: + reports = testdir.runitem( + """ + import pytest + def test_func(): + pytest.fail(pytrace=False) + """ + ) + rep = reports[1] + assert isinstance(rep.longrepr, _pytest._code.code.ExceptionRepr) + def test_outcome_exception_bad_msg() -> None: """Check that OutcomeExceptions validate their input to prevent confusing errors (#5578)""" diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 32634d78459..f48e7836459 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -194,7 +194,7 @@ def test_func(): assert len(reports) == 3 callreport = reports[1] assert callreport.failed - assert callreport.longrepr == "[XPASS(strict)] nope" + assert str(callreport.longrepr) == "[XPASS(strict)] nope" assert not hasattr(callreport, "wasxfail") def test_xfail_run_anyway(self, testdir): From 56bf819c2f4eaf8b36bd8c42c06bb59d5a3bfc0f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 30 May 2020 14:33:22 -0300 Subject: [PATCH 272/823] Do not call TestCase.tearDown for skipped tests (#7236) Fix #7215 --- changelog/7215.bugfix.rst | 2 ++ src/_pytest/unittest.py | 11 ++++++++--- testing/test_unittest.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 changelog/7215.bugfix.rst diff --git a/changelog/7215.bugfix.rst b/changelog/7215.bugfix.rst new file mode 100644 index 00000000000..81514913285 --- /dev/null +++ b/changelog/7215.bugfix.rst @@ -0,0 +1,2 @@ +Fix regression where running with ``--pdb`` would call the ``tearDown`` methods of ``unittest.TestCase`` +subclasses for skipped tests. diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 773f545af2e..0d9133f6023 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -41,7 +41,7 @@ def collect(self): if not getattr(cls, "__test__", True): return - skipped = getattr(cls, "__unittest_skip__", False) + skipped = _is_skipped(cls) if not skipped: self._inject_setup_teardown_fixtures(cls) self._inject_setup_class_fixture() @@ -89,7 +89,7 @@ def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self): @pytest.fixture(scope=scope, autouse=True) def fixture(self, request): - if getattr(self, "__unittest_skip__", None): + if _is_skipped(self): reason = self.__unittest_skip_why__ pytest.skip(reason) if setup is not None: @@ -220,7 +220,7 @@ def runtest(self): # arguably we could always postpone tearDown(), but this changes the moment where the # TestCase instance interacts with the results object, so better to only do it # when absolutely needed - if self.config.getoption("usepdb"): + if self.config.getoption("usepdb") and not _is_skipped(self.obj): self._explicit_tearDown = self._testcase.tearDown setattr(self._testcase, "tearDown", lambda *args: None) @@ -301,3 +301,8 @@ def check_testcase_implements_trial_reporter(done=[]): classImplements(TestCaseFunction, IReporter) done.append(1) + + +def _is_skipped(obj) -> bool: + """Return True if the given object has been marked with @unittest.skip""" + return bool(getattr(obj, "__unittest_skip__", False)) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 83f1b6b2a85..74a36c41bc0 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1193,6 +1193,40 @@ def test_2(self): ] +@pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"]) +def test_pdb_teardown_skipped(testdir, monkeypatch, mark): + """ + With --pdb, setUp and tearDown should not be called for skipped tests. + """ + tracked = [] + monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False) + + testdir.makepyfile( + """ + import unittest + import pytest + + class MyTestCase(unittest.TestCase): + + def setUp(self): + pytest.test_pdb_teardown_skipped.append("setUp:" + self.id()) + + def tearDown(self): + pytest.test_pdb_teardown_skipped.append("tearDown:" + self.id()) + + {mark}("skipped for reasons") + def test_1(self): + pass + + """.format( + mark=mark + ) + ) + result = testdir.runpytest_inprocess("--pdb") + result.stdout.fnmatch_lines("* 1 skipped in *") + assert tracked == [] + + def test_async_support(testdir): pytest.importorskip("unittest.async_case") From fb9f277a99c32dbdde251a1df1ae5bc64c4c7de6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 10 Jan 2020 00:50:12 +0100 Subject: [PATCH 273/823] Node._repr_failure_py: use abspath with changed cwd Fixes https://github.com/pytest-dev/pytest/issues/6428. --- src/_pytest/nodes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 4a79bc86157..61c6bc90ab4 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -362,8 +362,7 @@ def _repr_failure_py( truncate_locals = True try: - os.getcwd() - abspath = False + abspath = os.getcwd() != str(self.config.invocation_dir) except OSError: abspath = True From b98aa195e0cc3f468316358bcd49e7dbc4d13483 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 16 May 2020 11:36:22 -0300 Subject: [PATCH 274/823] Add test and changelog for #6428 --- changelog/6428.bugfix.rst | 2 ++ testing/test_nodes.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 changelog/6428.bugfix.rst diff --git a/changelog/6428.bugfix.rst b/changelog/6428.bugfix.rst new file mode 100644 index 00000000000..581b2b7cece --- /dev/null +++ b/changelog/6428.bugfix.rst @@ -0,0 +1,2 @@ +Paths appearing in error messages are now correct in case the current working directory has +changed since the start of the session. diff --git a/testing/test_nodes.py b/testing/test_nodes.py index dbb3e2e8f64..5bd31b34261 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -58,3 +58,30 @@ class FakeSession: outside = py.path.local("/outside") assert nodes._check_initialpaths_for_relpath(FakeSession, outside) is None + + +def test_failure_with_changed_cwd(testdir): + """ + Test failure lines should use absolute paths if cwd has changed since + invocation, so the path is correct (#6428). + """ + p = testdir.makepyfile( + """ + import os + import pytest + + @pytest.fixture + def private_dir(): + out_dir = 'ddd' + os.mkdir(out_dir) + old_dir = os.getcwd() + os.chdir(out_dir) + yield out_dir + os.chdir(old_dir) + + def test_show_wrong_path(private_dir): + assert False + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines([str(p) + ":*: AssertionError", "*1 failed in *"]) From 757bded13592ca05d124215124b5808d9290e063 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 16 May 2020 12:00:23 -0300 Subject: [PATCH 275/823] Use Path() instead of str for path comparison On Windows specifically is common to have drives diverging just by casing ("C:" vs "c:"), depending on the cwd provided by the user. --- src/_pytest/nodes.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 61c6bc90ab4..7a8c28cd4fd 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -29,6 +29,7 @@ from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords from _pytest.outcomes import fail +from _pytest.pathlib import Path from _pytest.store import Store if TYPE_CHECKING: @@ -361,8 +362,14 @@ def _repr_failure_py( else: truncate_locals = True + # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False. + # It is possible for a fixture/test to change the CWD while this code runs, which + # would then result in the user seeing confusing paths in the failure message. + # To fix this, if the CWD changed, always display the full absolute path. + # It will be better to just always display paths relative to invocation_dir, but + # this requires a lot of plumbing (#6428). try: - abspath = os.getcwd() != str(self.config.invocation_dir) + abspath = Path(os.getcwd()) != Path(self.config.invocation_dir) except OSError: abspath = True From eef4f87e7b9958687b6032f8475535ac0beca10f Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 30 May 2020 20:36:02 -0400 Subject: [PATCH 276/823] Output a warning to stderr when an invalid key is read from an INI config file --- src/_pytest/config/__init__.py | 9 +++++++ testing/test_config.py | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index bb5034ab1f2..65e5271c293 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1020,6 +1020,7 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: ) self._checkversion() + self._validatekeys() self._consider_importhook(args) self.pluginmanager.consider_preparse(args, exclude_only=False) if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): @@ -1072,6 +1073,14 @@ def _checkversion(self): ) ) + def _validatekeys(self): + for key in self._get_unknown_ini_keys(): + sys.stderr.write("WARNING: unknown config ini key: {}\n".format(key)) + + def _get_unknown_ini_keys(self) -> List[str]: + parser_inicfg = self._parser._inidict + return [name for name in self.inicfg if name not in parser_inicfg] + def parse(self, args: List[str], addopts: bool = True) -> None: # parse given cmdline arguments into this config object. assert not hasattr( diff --git a/testing/test_config.py b/testing/test_config.py index 17385dc17f5..9323e6716c5 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -147,6 +147,52 @@ def test_confcutdir(self, testdir): result = testdir.inline_run("--confcutdir=.") assert result.ret == 0 + @pytest.mark.parametrize( + "ini_file_text, invalid_keys, stderr_output", + [ + ( + """ + [pytest] + unknown_ini = value1 + another_unknown_ini = value2 + """, + ["unknown_ini", "another_unknown_ini"], + [ + "WARNING: unknown config ini key: unknown_ini", + "WARNING: unknown config ini key: another_unknown_ini", + ], + ), + ( + """ + [pytest] + unknown_ini = value1 + minversion = 5.0.0 + """, + ["unknown_ini"], + ["WARNING: unknown config ini key: unknown_ini"], + ), + ( + """ + [pytest] + minversion = 5.0.0 + """, + [], + [], + ), + ], + ) + def test_invalid_ini_keys_generate_warings( + self, testdir, ini_file_text, invalid_keys, stderr_output + ): + testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) + config = testdir.parseconfig() + assert config._get_unknown_ini_keys() == invalid_keys, str( + config._get_unknown_ini_keys() + ) + + result = testdir.runpytest() + result.stderr.fnmatch_lines(stderr_output) + class TestConfigCmdlineParsing: def test_parsing_again_fails(self, testdir): From 8f2c2a5dd9fffc2a59d3ed868c801746ce4b51b5 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 31 May 2020 00:49:21 -0400 Subject: [PATCH 277/823] Add test case for invalid ini key in different section header --- testing/test_config.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/testing/test_config.py b/testing/test_config.py index 9323e6716c5..e350193376d 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -173,6 +173,16 @@ def test_confcutdir(self, testdir): ), ( """ + [some_other_header] + unknown_ini = value1 + [pytest] + minversion = 5.0.0 + """, + [], + [], + ), + ( + """ [pytest] minversion = 5.0.0 """, From b32f4de891e217a24f02bb66a63750a36d7effac Mon Sep 17 00:00:00 2001 From: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Date: Sun, 31 May 2020 08:37:26 +0200 Subject: [PATCH 278/823] Issue 7202 - Point development guide to contributing section (#7280) --- changelog/7202.doc.rst | 1 + doc/en/development_guide.rst | 59 ++---------------------------------- 2 files changed, 4 insertions(+), 56 deletions(-) create mode 100644 changelog/7202.doc.rst diff --git a/changelog/7202.doc.rst b/changelog/7202.doc.rst new file mode 100644 index 00000000000..143f28d4080 --- /dev/null +++ b/changelog/7202.doc.rst @@ -0,0 +1 @@ +The development guide now links to the contributing section of the docs and 'RELEASING.rst' on GitHub. diff --git a/doc/en/development_guide.rst b/doc/en/development_guide.rst index 2f9762f2a84..77076d4834e 100644 --- a/doc/en/development_guide.rst +++ b/doc/en/development_guide.rst @@ -2,59 +2,6 @@ Development Guide ================= -Some general guidelines regarding development in pytest for maintainers and contributors. Nothing here -is set in stone and can't be changed, feel free to suggest improvements or changes in the workflow. - - -Code Style ----------- - -* `PEP-8 `_ -* `flake8 `_ for quality checks -* `invoke `_ to automate development tasks - - -Branches --------- - -We have two long term branches: - -* ``master``: contains the code for the next bug-fix release. -* ``features``: contains the code with new features for the next minor release. - -The official repository usually does not contain topic branches, developers and contributors should create topic -branches in their own forks. - -Exceptions can be made for cases where more than one contributor is working on the same -topic or where it makes sense to use some automatic capability of the main repository, such as automatic docs from -`readthedocs `_ for a branch dealing with documentation refactoring. - -Issues ------- - -Any question, feature, bug or proposal is welcome as an issue. Users are encouraged to use them whenever they need. - -GitHub issues should use labels to categorize them. Labels should be created sporadically, to fill a niche; we should -avoid creating labels just for the sake of creating them. - -Each label should include a description in the GitHub's interface stating its purpose. - -Labels are managed using `labels `_. All the labels in the repository -are kept in ``.github/labels.toml``, so any changes should be via PRs to that file. -After a PR is accepted and merged, one of the maintainers must manually synchronize the labels file with the -GitHub repository. - -Temporary labels -~~~~~~~~~~~~~~~~ - -To classify issues for a special event it is encouraged to create a temporary label. This helps those involved to find -the relevant issues to work on. Examples of that are sprints in Python events or global hacking events. - -* ``temporary: EP2017 sprint``: candidate issues or PRs tackled during the EuroPython 2017 - -Issues created at those events should have other relevant labels added as well. - -Those labels should be removed after they are no longer relevant. - - -.. include:: ../../RELEASING.rst +The contributing guidelines are to be found :ref:`here `. +The release procedure for pytest is documented on +`GitHub `_. From db203afba32cc162ab9e8d1da079dde600bd21cc Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 31 May 2020 02:45:40 -0400 Subject: [PATCH 279/823] Add in --strict-config flag to force warnings to errors --- src/_pytest/config/__init__.py | 9 ++++++--- src/_pytest/main.py | 5 +++++ testing/test_config.py | 20 ++++++++++++++------ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 65e5271c293..63f7d4cb43a 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1020,7 +1020,7 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: ) self._checkversion() - self._validatekeys() + self._validatekeys(args) self._consider_importhook(args) self.pluginmanager.consider_preparse(args, exclude_only=False) if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): @@ -1073,9 +1073,12 @@ def _checkversion(self): ) ) - def _validatekeys(self): + def _validatekeys(self, args: Sequence[str]): for key in self._get_unknown_ini_keys(): - sys.stderr.write("WARNING: unknown config ini key: {}\n".format(key)) + message = "Unknown config ini key: {}\n".format(key) + if "--strict-config" in args: + fail(message, pytrace=False) + sys.stderr.write("WARNING: {}".format(message)) def _get_unknown_ini_keys(self) -> List[str]: parser_inicfg = self._parser._inidict diff --git a/src/_pytest/main.py b/src/_pytest/main.py index de7e16744a4..4eb47be2cde 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -70,6 +70,11 @@ def pytest_addoption(parser): default=0, help="exit after first num failures or errors.", ) + group._addoption( + "--strict-config", + action="store_true", + help="invalid ini keys for the `pytest` section of the configuration file raise errors.", + ) group._addoption( "--strict-markers", "--strict", diff --git a/testing/test_config.py b/testing/test_config.py index e350193376d..6a08e93f39c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -148,7 +148,7 @@ def test_confcutdir(self, testdir): assert result.ret == 0 @pytest.mark.parametrize( - "ini_file_text, invalid_keys, stderr_output", + "ini_file_text, invalid_keys, stderr_output, exception_text", [ ( """ @@ -158,9 +158,10 @@ def test_confcutdir(self, testdir): """, ["unknown_ini", "another_unknown_ini"], [ - "WARNING: unknown config ini key: unknown_ini", - "WARNING: unknown config ini key: another_unknown_ini", + "WARNING: Unknown config ini key: unknown_ini", + "WARNING: Unknown config ini key: another_unknown_ini", ], + "Unknown config ini key: unknown_ini", ), ( """ @@ -169,7 +170,8 @@ def test_confcutdir(self, testdir): minversion = 5.0.0 """, ["unknown_ini"], - ["WARNING: unknown config ini key: unknown_ini"], + ["WARNING: Unknown config ini key: unknown_ini"], + "Unknown config ini key: unknown_ini", ), ( """ @@ -180,6 +182,7 @@ def test_confcutdir(self, testdir): """, [], [], + "", ), ( """ @@ -188,11 +191,12 @@ def test_confcutdir(self, testdir): """, [], [], + "", ), ], ) - def test_invalid_ini_keys_generate_warings( - self, testdir, ini_file_text, invalid_keys, stderr_output + def test_invalid_ini_keys( + self, testdir, ini_file_text, invalid_keys, stderr_output, exception_text ): testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) config = testdir.parseconfig() @@ -203,6 +207,10 @@ def test_invalid_ini_keys_generate_warings( result = testdir.runpytest() result.stderr.fnmatch_lines(stderr_output) + if stderr_output: + with pytest.raises(pytest.fail.Exception, match=exception_text): + testdir.runpytest("--strict-config") + class TestConfigCmdlineParsing: def test_parsing_again_fails(self, testdir): From 9da1d0687ea02b724ad27c1caafc58651e01ee5a Mon Sep 17 00:00:00 2001 From: Simon K Date: Sun, 31 May 2020 16:11:11 +0100 Subject: [PATCH 280/823] adding towncrier wrapper script so 'tox -e docs' works natively on windows (#7266) * enable tox -e docs natively on windows using a wrapper * rename the towncrier script; run the towncrier command in a safer manner * use subprocess.call; call exit() around main on towncrier wrapper * change to sys.exit() instead of builtin exit() --- scripts/towncrier-draft-to-file.py | 15 +++++++++++++++ tox.ini | 3 +-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 scripts/towncrier-draft-to-file.py diff --git a/scripts/towncrier-draft-to-file.py b/scripts/towncrier-draft-to-file.py new file mode 100644 index 00000000000..81507b40b75 --- /dev/null +++ b/scripts/towncrier-draft-to-file.py @@ -0,0 +1,15 @@ +import sys +from subprocess import call + + +def main(): + """ + Platform agnostic wrapper script for towncrier. + Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs. + """ + with open("doc/en/_changelog_towncrier_draft.rst", "w") as draft_file: + return call(("towncrier", "--draft"), stdout=draft_file) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tox.ini b/tox.ini index f363f57012e..8e1a51ca760 100644 --- a/tox.ini +++ b/tox.ini @@ -80,9 +80,8 @@ usedevelop = True deps = -r{toxinidir}/doc/en/requirements.txt towncrier -whitelist_externals = sh commands = - sh -c 'towncrier --draft > doc/en/_changelog_towncrier_draft.rst' + python scripts/towncrier-draft-to-file.py # the '-t changelog_towncrier_draft' tags makes sphinx include the draft # 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 From 92d15c6af1943faeb629bf3cbd6488b56d9aef52 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 31 May 2020 11:33:31 -0400 Subject: [PATCH 281/823] review feedback --- src/_pytest/config/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 63f7d4cb43a..5fc23716dc3 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1020,7 +1020,6 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: ) self._checkversion() - self._validatekeys(args) self._consider_importhook(args) self.pluginmanager.consider_preparse(args, exclude_only=False) if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): @@ -1031,6 +1030,7 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: self.known_args_namespace = ns = self._parser.parse_known_args( args, namespace=copy.copy(self.option) ) + self._validatekeys() if self.known_args_namespace.confcutdir is None and self.inifile: confcutdir = py.path.local(self.inifile).dirname self.known_args_namespace.confcutdir = confcutdir @@ -1073,10 +1073,10 @@ def _checkversion(self): ) ) - def _validatekeys(self, args: Sequence[str]): + def _validatekeys(self): for key in self._get_unknown_ini_keys(): message = "Unknown config ini key: {}\n".format(key) - if "--strict-config" in args: + if self.known_args_namespace.strict_config: fail(message, pytrace=False) sys.stderr.write("WARNING: {}".format(message)) From 9ae94b08e2ad55aed1abc7aa414adac626d005f9 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 31 May 2020 11:58:39 -0400 Subject: [PATCH 282/823] Add documentation --- AUTHORS | 1 + changelog/6856.feature.rst | 0 2 files changed, 1 insertion(+) create mode 100644 changelog/6856.feature.rst diff --git a/AUTHORS b/AUTHORS index 5d410da18bb..41b0e38b055 100644 --- a/AUTHORS +++ b/AUTHORS @@ -109,6 +109,7 @@ Gabriel Reis Gene Wood George Kussumoto Georgy Dyuldin +Gleb Nikonorov Graham Horler Greg Price Gregory Lee diff --git a/changelog/6856.feature.rst b/changelog/6856.feature.rst new file mode 100644 index 00000000000..e69de29bb2d From 2f406bb9cb8538e5a43551d6eeffe2be80bccefa Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 1 Jun 2020 09:21:08 -0300 Subject: [PATCH 283/823] Replace custom flask theme by the official one (#6453) Ref: #6402 --- .../flask => _templates}/relations.html | 0 .../flask => _templates}/slim_searchbox.html | 0 doc/en/_themes/.gitignore | 3 - doc/en/_themes/LICENSE | 37 -- doc/en/_themes/README | 31 - doc/en/_themes/flask/layout.html | 24 - doc/en/_themes/flask/static/flasky.css_t | 623 ------------------ doc/en/_themes/flask/theme.conf | 9 - doc/en/_themes/flask_theme_support.py | 87 --- doc/en/conf.py | 3 +- doc/en/requirements.txt | 3 +- 11 files changed, 4 insertions(+), 816 deletions(-) rename doc/en/{_themes/flask => _templates}/relations.html (100%) rename doc/en/{_themes/flask => _templates}/slim_searchbox.html (100%) delete mode 100644 doc/en/_themes/.gitignore delete mode 100644 doc/en/_themes/LICENSE delete mode 100644 doc/en/_themes/README delete mode 100644 doc/en/_themes/flask/layout.html delete mode 100644 doc/en/_themes/flask/static/flasky.css_t delete mode 100644 doc/en/_themes/flask/theme.conf delete mode 100644 doc/en/_themes/flask_theme_support.py diff --git a/doc/en/_themes/flask/relations.html b/doc/en/_templates/relations.html similarity index 100% rename from doc/en/_themes/flask/relations.html rename to doc/en/_templates/relations.html diff --git a/doc/en/_themes/flask/slim_searchbox.html b/doc/en/_templates/slim_searchbox.html similarity index 100% rename from doc/en/_themes/flask/slim_searchbox.html rename to doc/en/_templates/slim_searchbox.html diff --git a/doc/en/_themes/.gitignore b/doc/en/_themes/.gitignore deleted file mode 100644 index 66b6e4c2f3b..00000000000 --- a/doc/en/_themes/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.pyc -*.pyo -.DS_Store diff --git a/doc/en/_themes/LICENSE b/doc/en/_themes/LICENSE deleted file mode 100644 index 8daab7ee6ef..00000000000 --- a/doc/en/_themes/LICENSE +++ /dev/null @@ -1,37 +0,0 @@ -Copyright (c) 2010 by Armin Ronacher. - -Some rights reserved. - -Redistribution and use in source and binary forms of the theme, with or -without modification, are permitted provided that the following conditions -are met: - -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - -* The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -We kindly ask you to only use these themes in an unmodified manner just -for Flask and Flask-related products, not for unrelated projects. If you -like the visual style and want to use it for your own projects, please -consider making some larger changes to the themes (such as changing -font faces, sizes, colors or margins). - -THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/doc/en/_themes/README b/doc/en/_themes/README deleted file mode 100644 index b3292bdff8e..00000000000 --- a/doc/en/_themes/README +++ /dev/null @@ -1,31 +0,0 @@ -Flask Sphinx Styles -=================== - -This repository contains sphinx styles for Flask and Flask related -projects. To use this style in your Sphinx documentation, follow -this guide: - -1. put this folder as _themes into your docs folder. Alternatively - you can also use git submodules to check out the contents there. -2. add this to your conf.py: - - sys.path.append(os.path.abspath('_themes')) - html_theme_path = ['_themes'] - html_theme = 'flask' - -The following themes exist: - -- 'flask' - the standard flask documentation theme for large - projects -- 'flask_small' - small one-page theme. Intended to be used by - very small addon libraries for flask. - -The following options exist for the flask_small theme: - - [options] - index_logo = '' filename of a picture in _static - to be used as replacement for the - h1 in the index.rst file. - index_logo_height = 120px height of the index logo - github_fork = '' repository name on github for the - "fork me" badge diff --git a/doc/en/_themes/flask/layout.html b/doc/en/_themes/flask/layout.html deleted file mode 100644 index f2fa8e6aa9a..00000000000 --- a/doc/en/_themes/flask/layout.html +++ /dev/null @@ -1,24 +0,0 @@ -{%- extends "basic/layout.html" %} -{%- block extrahead %} - {{ super() }} - {% if theme_touch_icon %} - - {% endif %} - -{% endblock %} -{%- block relbar2 %}{% endblock %} -{% block header %} - {{ super() }} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{%- block footer %} - - {% if pagename == 'index' %} -
- {% endif %} -{%- endblock %} diff --git a/doc/en/_themes/flask/static/flasky.css_t b/doc/en/_themes/flask/static/flasky.css_t deleted file mode 100644 index 108c8540157..00000000000 --- a/doc/en/_themes/flask/static/flasky.css_t +++ /dev/null @@ -1,623 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * :copyright: Copyright 2010 by Armin Ronacher. - * :license: Flask Design License, see LICENSE for details. - */ - -{% set page_width = '1020px' %} -{% set sidebar_width = '220px' %} -/* muted version of green logo color #C9D22A */ -{% set link_color = '#606413' %} -/* blue logo color */ -{% set link_hover_color = '#009de0' %} -{% set base_font = 'sans-serif' %} -{% set header_font = 'sans-serif' %} - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: {{ base_font }}; - font-size: 16px; - background-color: white; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - width: {{ page_width }}; - margin: 30px auto 0 auto; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 {{ sidebar_width }}; -} - -div.sphinxsidebar { - width: {{ sidebar_width }}; -} - -hr { - border: 0; - border-top: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 0 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - width: {{ page_width }}; - margin: 20px auto 30px auto; - font-size: 14px; - color: #888; - text-align: right; -} - -div.footer a { - color: #888; -} - -div.related { - display: none; -} - -div.sphinxsidebar a { - text-decoration: none; - border-bottom: none; -} - -div.sphinxsidebar a:hover { - color: {{ link_hover_color }}; - border-bottom: 1px solid {{ link_hover_color }}; -} - -div.sphinxsidebar { - font-size: 14px; - line-height: 1.5; -} - -div.sphinxsidebarwrapper { - padding: 18px 10px; -} - -div.sphinxsidebarwrapper p.logo { - padding: 0 0 20px 0; - margin: 0; - text-align: center; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: {{ header_font }}; - color: #444; - font-size: 21px; - font-weight: normal; - margin: 16px 0 0 0; - padding: 0; -} - -div.sphinxsidebar h4 { - font-size: 18px; -} - -div.sphinxsidebar h3 a { - color: #444; -} - -div.sphinxsidebar p.logo a, -div.sphinxsidebar h3 a, -div.sphinxsidebar p.logo a:hover, -div.sphinxsidebar h3 a:hover { - border: none; -} - -div.sphinxsidebar p { - color: #555; - margin: 10px 0; -} - -div.sphinxsidebar ul { - margin: 10px 0; - padding: 0; - color: #000; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: {{ base_font }}; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: {{ link_color }}; - text-decoration: underline; -} - -a:hover { - color: {{ link_hover_color }}; - text-decoration: underline; -} - -a.reference.internal em { - font-style: normal; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: {{ header_font }}; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% else %} -div.indexwrapper div.body h1 { - font-size: 200%; -} -{% endif %} -div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #ddd; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -ul.simple li { - margin-bottom: 0.5em; -} - -div.topic ul.simple li { - margin-bottom: 0; -} - -div.topic li > p:first-child { - margin-top: 0; - margin-bottom: 0; -} - -div.admonition { - background: #fafafa; - padding: 10px 20px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition tt.xref, div.admonition a tt { - border-bottom: 1px solid #fafafa; -} - -div.admonition p.admonition-title { - font-family: {{ header_font }}; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition :last-child { - margin-bottom: 0; -} - -div.highlight { - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note, div.warning { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.topic a { - text-decoration: none; - border-bottom: none; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt, code { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.9em; - background: #eee; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; - background: #fdfdfd; - font-size: 0.9em; -} - -table.footnote + table.footnote { - margin-top: -15px; - border-top: none; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td.label { - width: 0px; - padding: 0.3em 0 0.3em 0.5em; -} - -table.footnote td { - padding: 0.3em 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -blockquote { - margin: 0 0 0 30px; - padding: 0; -} - -ul, ol { - margin: 10px 0 10px 30px; - padding: 0; -} - -pre { - background: #eee; - padding: 7px 12px; - line-height: 1.3em; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; - border-bottom: 1px solid white; -} - -a.reference { - text-decoration: none; - border-bottom: 1px dotted {{ link_color }}; -} - -a.reference:hover { - border-bottom: 1px solid {{ link_hover_color }}; -} - -li.toctree-l1 a.reference, -li.toctree-l2 a.reference, -li.toctree-l3 a.reference, -li.toctree-l4 a.reference { - border-bottom: none; -} - -li.toctree-l1 a.reference:hover, -li.toctree-l2 a.reference:hover, -li.toctree-l3 a.reference:hover, -li.toctree-l4 a.reference:hover { - border-bottom: 1px solid {{ link_hover_color }}; -} - -a.footnote-reference { - text-decoration: none; - font-size: 0.7em; - vertical-align: top; - border-bottom: 1px dotted {{ link_color }}; -} - -a.footnote-reference:hover { - border-bottom: 1px solid {{ link_hover_color }}; -} - -a:hover tt { - background: #EEE; -} - -#reference div.section h2 { - /* separate code elements in the reference section */ - border-top: 2px solid #ccc; - padding-top: 0.5em; -} - -#reference div.section h3 { - /* separate code elements in the reference section */ - border-top: 1px solid #ccc; - padding-top: 0.5em; -} - -dl.class, dl.function { - margin-top: 1em; - margin-bottom: 1em; -} - -dl.class > dd { - border-left: 3px solid #ccc; - margin-left: 0px; - padding-left: 30px; -} - -dl.field-list { - flex-direction: column; -} - -dl.field-list dd { - padding-left: 4em; - border-left: 3px solid #ccc; - margin-bottom: 0.5em; -} - -dl.field-list dd > ul { - list-style: none; - padding-left: 0px; -} - -dl.field-list dd > ul > li li :first-child { - text-indent: 0; -} - -dl.field-list dd > ul > li :first-child { - text-indent: -2em; - padding-left: 0px; -} - -dl.field-list dd > p:first-child { - text-indent: -2em; -} - -@media screen and (max-width: 870px) { - - div.sphinxsidebar { - display: none; - } - - div.document { - width: 100%; - - } - - div.documentwrapper { - margin-left: 0; - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - } - - div.bodywrapper { - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - margin-left: 0; - } - - ul { - margin-left: 0; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .bodywrapper { - margin: 0; - } - - .footer { - width: auto; - } - - .github { - display: none; - } - - - -} - - - -@media screen and (max-width: 875px) { - - body { - margin: 0; - padding: 20px 30px; - } - - div.documentwrapper { - float: none; - background: white; - } - - div.sphinxsidebar { - display: block; - float: none; - width: 102.5%; - margin: 50px -30px -20px -30px; - padding: 10px 20px; - background: #333; - color: white; - } - - div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, - div.sphinxsidebar h3 a, div.sphinxsidebar ul { - color: white; - } - - div.sphinxsidebar a { - color: #aaa; - } - - div.sphinxsidebar p.logo { - display: none; - } - - div.document { - width: 100%; - margin: 0; - } - - div.related { - display: block; - margin: 0; - padding: 10px 0 20px 0; - } - - div.related ul, - div.related ul li { - margin: 0; - padding: 0; - } - - div.footer { - display: none; - } - - div.bodywrapper { - margin: 0; - } - - div.body { - min-height: 0; - padding: 0; - } - - .rtd_doc_footer { - display: none; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .footer { - width: auto; - } - - .github { - display: none; - } -} - -/* misc. */ - -.revsys-inline { - display: none!important; -} diff --git a/doc/en/_themes/flask/theme.conf b/doc/en/_themes/flask/theme.conf deleted file mode 100644 index 372b0028393..00000000000 --- a/doc/en/_themes/flask/theme.conf +++ /dev/null @@ -1,9 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = '' -index_logo_height = 120px -touch_icon = diff --git a/doc/en/_themes/flask_theme_support.py b/doc/en/_themes/flask_theme_support.py deleted file mode 100644 index b107f2c892e..00000000000 --- a/doc/en/_themes/flask_theme_support.py +++ /dev/null @@ -1,87 +0,0 @@ -# flasky extensions. flasky pygments style based on tango style -from pygments.style import Style -from pygments.token import Comment -from pygments.token import Error -from pygments.token import Generic -from pygments.token import Keyword -from pygments.token import Literal -from pygments.token import Name -from pygments.token import Number -from pygments.token import Operator -from pygments.token import Other -from pygments.token import Punctuation -from pygments.token import String -from pygments.token import Whitespace - - -class FlaskyStyle(Style): - background_color = "#f8f8f8" - default_style = "" - - styles = { - # No corresponding class for the following: - # Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - Punctuation: "bold #000000", # class: 'p' - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - Number: "#990000", # class: 'm' - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } diff --git a/doc/en/conf.py b/doc/en/conf.py index e62bc157a2e..72e2d4f20f1 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -43,6 +43,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ + "pallets_sphinx_themes", "pygments_pytest", "sphinx.ext.autodoc", "sphinx.ext.autosummary", @@ -142,7 +143,7 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = {"index_logo": None} +# html_theme_options = {"index_logo": None} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] diff --git a/doc/en/requirements.txt b/doc/en/requirements.txt index be22b7db872..1e5e7efdc76 100644 --- a/doc/en/requirements.txt +++ b/doc/en/requirements.txt @@ -1,4 +1,5 @@ +pallets-sphinx-themes pygments-pytest>=1.1.0 +sphinx-removed-in>=0.2.0 sphinx>=1.8.2,<2.1 sphinxcontrib-trio -sphinx-removed-in>=0.2.0 From 2748feed38f826057f85d14229557304dbdfb26d Mon Sep 17 00:00:00 2001 From: Keri Volans Date: Mon, 1 Jun 2020 16:19:40 +0100 Subject: [PATCH 284/823] 7291: Replace py.iniconfig with iniconfig --- changelog/7291.trivial.rst | 1 + setup.py | 1 + src/_pytest/config/findpaths.py | 8 +++++--- src/_pytest/pytester.py | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 changelog/7291.trivial.rst diff --git a/changelog/7291.trivial.rst b/changelog/7291.trivial.rst new file mode 100644 index 00000000000..9bc99f6517b --- /dev/null +++ b/changelog/7291.trivial.rst @@ -0,0 +1 @@ +Replaced usages of py.iniconfig with iniconfig. diff --git a/setup.py b/setup.py index cd2ecbe07f7..79fef1f4dda 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ 'colorama;sys_platform=="win32"', "pluggy>=0.12,<1.0", 'importlib-metadata>=0.12;python_version<"3.8"', + "iniconfig", ] diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index f4f62e06b8f..2b252c4f474 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -6,6 +6,8 @@ from typing import Tuple import py +from iniconfig import IniConfig +from iniconfig import ParseError from .exceptions import UsageError from _pytest.compat import TYPE_CHECKING @@ -40,8 +42,8 @@ def getcfg(args, config=None): p = base.join(inibasename) if exists(p): try: - iniconfig = py.iniconfig.IniConfig(p) - except py.iniconfig.ParseError as exc: + iniconfig = IniConfig(p) + except ParseError as exc: raise UsageError(str(exc)) if ( @@ -119,7 +121,7 @@ def determine_setup( ) -> Tuple[py.path.local, Optional[str], Any]: dirs = get_dirs_from_args(args) if inifile: - iniconfig = py.iniconfig.IniConfig(inifile) + iniconfig = IniConfig(inifile) is_cfg_file = str(inifile).endswith(".cfg") sections = ["tool:pytest", "pytest"] if is_cfg_file else ["pytest"] for section in sections: diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 9df86a22fa3..fc4e4d853c3 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -22,6 +22,7 @@ from weakref import WeakKeyDictionary import py +from iniconfig import IniConfig import pytest from _pytest._code import Source @@ -683,7 +684,7 @@ def makeini(self, source): def getinicfg(self, source): """Return the pytest section from the tox.ini config file.""" p = self.makeini(source) - return py.iniconfig.IniConfig(p)["pytest"] + return IniConfig(p)["pytest"] def makepyfile(self, *args, **kwargs): r"""Shortcut for .makefile() with a .py extension. From 9214e63af38ad76c6d4f02f0db4a4cdb736ab032 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 2 Jun 2020 10:29:36 +0300 Subject: [PATCH 285/823] ci: use fetch-depth: 0 instead of fetching manually (#7297) --- .github/workflows/main.yml | 8 ++++---- .github/workflows/release-on-comment.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6d1910014ed..262ed59463b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -128,8 +128,8 @@ jobs: steps: - uses: actions/checkout@v2 - # For setuptools-scm. - - run: git fetch --prune --unshallow + with: + fetch-depth: 0 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v2 if: matrix.python != '3.9-dev' @@ -177,8 +177,8 @@ jobs: steps: - uses: actions/checkout@v2 - # For setuptools-scm. - - run: git fetch --prune --unshallow + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v2 with: diff --git a/.github/workflows/release-on-comment.yml b/.github/workflows/release-on-comment.yml index 9d803cd38dd..94863d896b9 100644 --- a/.github/workflows/release-on-comment.yml +++ b/.github/workflows/release-on-comment.yml @@ -15,8 +15,8 @@ jobs: steps: - uses: actions/checkout@v2 - # For setuptools-scm. - - run: git fetch --prune --unshallow + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v2 From eaf46f53545f4fd8f7d6bd64115c8751590a555c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 1 Jun 2020 21:09:40 -0300 Subject: [PATCH 286/823] Adjust codecov: only patch statuses Fix #6994 --- codecov.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index db2472009c6..f1cc8697338 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1 +1,6 @@ -comment: off +# reference: https://docs.codecov.io/docs/codecovyml-reference +coverage: + status: + patch: true + project: false +comment: false From fe640934111b52b0461a3d35994730667525b783 Mon Sep 17 00:00:00 2001 From: Tor Colvin Date: Tue, 2 Jun 2020 07:56:33 -0400 Subject: [PATCH 287/823] Fix removal of very long paths on Windows (#6755) Co-authored-by: Bruno Oliveira --- AUTHORS | 1 + changelog/6755.bugfix.rst | 1 + src/_pytest/pathlib.py | 32 ++++++++++++++++++++++++++++++++ testing/test_pathlib.py | 24 ++++++++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 changelog/6755.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 5d410da18bb..a76c0a632c9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -277,6 +277,7 @@ Tom Dalton Tom Viner Tomáš Gavenčiak Tomer Keren +Tor Colvin Trevor Bekolay Tyler Goodlet Tzu-ping Chung diff --git a/changelog/6755.bugfix.rst b/changelog/6755.bugfix.rst new file mode 100644 index 00000000000..8077baa4f55 --- /dev/null +++ b/changelog/6755.bugfix.rst @@ -0,0 +1 @@ +Support deleting paths longer than 260 characters on windows created inside tmpdir. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 21ec61e2c5b..90a7460b029 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -100,10 +100,41 @@ def chmod_rw(p: str) -> None: return True +def ensure_extended_length_path(path: Path) -> Path: + """Get the extended-length version of a path (Windows). + + On Windows, by default, the maximum length of a path (MAX_PATH) is 260 + characters, and operations on paths longer than that fail. But it is possible + to overcome this by converting the path to "extended-length" form before + performing the operation: + https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation + + On Windows, this function returns the extended-length absolute version of path. + On other platforms it returns path unchanged. + """ + if sys.platform.startswith("win32"): + path = path.resolve() + path = Path(get_extended_length_path_str(str(path))) + return path + + +def get_extended_length_path_str(path: str) -> str: + """Converts to extended length path as a str""" + long_path_prefix = "\\\\?\\" + unc_long_path_prefix = "\\\\?\\UNC\\" + if path.startswith((long_path_prefix, unc_long_path_prefix)): + return path + # UNC + if path.startswith("\\\\"): + return unc_long_path_prefix + path[2:] + return long_path_prefix + path + + def rm_rf(path: Path) -> None: """Remove the path contents recursively, even if some elements are read-only. """ + path = ensure_extended_length_path(path) onerror = partial(on_rm_rf_error, start_path=path) shutil.rmtree(str(path), onerror=onerror) @@ -220,6 +251,7 @@ def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> Non def maybe_delete_a_numbered_dir(path: Path) -> None: """removes a numbered directory if its lock can be obtained and it does not seem to be in use""" + path = ensure_extended_length_path(path) lock_path = None try: lock_path = create_cleanup_lock(path) diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 45daeaed76a..03bed26ec3a 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -5,6 +5,7 @@ import pytest from _pytest.pathlib import fnmatch_ex +from _pytest.pathlib import get_extended_length_path_str from _pytest.pathlib import get_lock_path from _pytest.pathlib import maybe_delete_a_numbered_dir from _pytest.pathlib import Path @@ -89,3 +90,26 @@ def renamed_failed(*args): lock_path = get_lock_path(path) maybe_delete_a_numbered_dir(path) assert not lock_path.is_file() + + +def test_long_path_during_cleanup(tmp_path): + """Ensure that deleting long path works (particularly on Windows (#6775)).""" + path = (tmp_path / ("a" * 250)).resolve() + if sys.platform == "win32": + # make sure that the full path is > 260 characters without any + # component being over 260 characters + assert len(str(path)) > 260 + extended_path = "\\\\?\\" + str(path) + else: + extended_path = str(path) + os.mkdir(extended_path) + assert os.path.isdir(extended_path) + maybe_delete_a_numbered_dir(path) + assert not os.path.isdir(extended_path) + + +def test_get_extended_length_path_str(): + assert get_extended_length_path_str(r"c:\foo") == r"\\?\c:\foo" + assert get_extended_length_path_str(r"\\share\foo") == r"\\?\UNC\share\foo" + assert get_extended_length_path_str(r"\\?\UNC\share\foo") == r"\\?\UNC\share\foo" + assert get_extended_length_path_str(r"\\?\c:\foo") == r"\\?\c:\foo" From a5d13d4ced6dc3f8d826233e02788b86aebb3743 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Tue, 2 Jun 2020 08:21:57 -0400 Subject: [PATCH 288/823] Add changelog entry --- changelog/6856.feature.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog/6856.feature.rst b/changelog/6856.feature.rst index e69de29bb2d..36892fa21b8 100644 --- a/changelog/6856.feature.rst +++ b/changelog/6856.feature.rst @@ -0,0 +1,3 @@ +A warning is now shown when an unknown key is read from a config INI file. + +The `--strict-config` flag has been added to treat these warnings as errors. From 5517f7264fcb85cf1563702bd9444e7be917daf3 Mon Sep 17 00:00:00 2001 From: xuiqzy Date: Tue, 2 Jun 2020 14:56:39 +0200 Subject: [PATCH 289/823] Remove doc line that is no longer relevant for Python3-only (#7263) * Fix typo in capture.rst documentation Rename ``capfsysbinary`` to ``capsysbinary`` as the former does not exist as far as i can see. * Make Python uppercase in doc/en/capture.rst Co-authored-by: Hugo van Kemenade * Remove the sentence entirely Co-authored-by: Hugo van Kemenade Co-authored-by: Ran Benita --- doc/en/capture.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 7c8c25cc5c9..44d3a3bd175 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -145,8 +145,7 @@ The return value from ``readouterr`` changed to a ``namedtuple`` with two attrib If the code under test writes non-textual data, you can capture this using the ``capsysbinary`` fixture which instead returns ``bytes`` from -the ``readouterr`` method. The ``capfsysbinary`` fixture is currently only -available in python 3. +the ``readouterr`` method. From da5851c13e3dd992deb8267110e3099fc0216ced Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 2 Jun 2020 11:01:37 -0300 Subject: [PATCH 290/823] Update changelog/7291.trivial.rst --- changelog/7291.trivial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/7291.trivial.rst b/changelog/7291.trivial.rst index 9bc99f6517b..8f41528aa56 100644 --- a/changelog/7291.trivial.rst +++ b/changelog/7291.trivial.rst @@ -1 +1 @@ -Replaced usages of py.iniconfig with iniconfig. +Replaced ``py.iniconfig`` with `iniconfig `__. From 85b5a289f01696ad9131213bd6045867b46c50c2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 2 Jun 2020 19:59:25 +0300 Subject: [PATCH 291/823] warnings: fix missing None in existing hook & add some docs (#7288) --- doc/en/deprecations.rst | 11 +++++++++++ src/_pytest/hookspec.py | 27 +++++++++++++++++++-------- src/_pytest/warnings.py | 7 ++++++- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 0b7d3fecdab..dba02248bfd 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -20,6 +20,17 @@ Below is a complete list of all pytest features which are considered deprecated. :ref:`standard warning filters `. +The ``pytest_warning_captured`` hook +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.0 + +This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``. + +Use the ``pytest_warning_recored`` hook instead, which replaces the ``item`` parameter +by a ``nodeid`` parameter. + + The ``pytest._fillfuncargs`` function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 341f0a250b2..de29a40bfe9 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -623,9 +623,16 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): @hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK) -def pytest_warning_captured(warning_message, when, item, location): +def pytest_warning_captured( + warning_message: "warnings.WarningMessage", + when: str, + item, + location: Optional[Tuple[str, int, str]], +) -> None: """(**Deprecated**) Process a warning captured by the internal pytest warnings plugin. + .. deprecated:: 6.0 + This hook is considered deprecated and will be removed in a future pytest version. Use :func:`pytest_warning_recorded` instead. @@ -644,8 +651,9 @@ def pytest_warning_captured(warning_message, when, item, location): The item being executed if ``when`` is ``"runtest"``, otherwise ``None``. :param tuple location: - Holds information about the execution context of the captured warning (filename, linenumber, function). - ``function`` evaluates to when the execution context is at the module level. + When available, holds information about the execution context of the captured + warning (filename, linenumber, function). ``function`` evaluates to + when the execution context is at the module level. """ @@ -654,8 +662,8 @@ def pytest_warning_recorded( warning_message: "warnings.WarningMessage", when: str, nodeid: str, - location: Tuple[str, int, str], -): + location: Optional[Tuple[str, int, str]], +) -> None: """ Process a warning captured by the internal pytest warnings plugin. @@ -672,9 +680,12 @@ def pytest_warning_recorded( :param str nodeid: full id of the item - :param tuple location: - Holds information about the execution context of the captured warning (filename, linenumber, function). - ``function`` evaluates to when the execution context is at the module level. + :param tuple|None location: + When available, holds information about the execution context of the captured + warning (filename, linenumber, function). ``function`` evaluates to + when the execution context is at the module level. + + .. versionadded:: 6.0 """ diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 8828a53d611..33d89428be8 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -112,7 +112,12 @@ def catch_warnings_for_item(config, ihook, when, item): for warning_message in log: ihook.pytest_warning_captured.call_historic( - kwargs=dict(warning_message=warning_message, when=when, item=item) + kwargs=dict( + warning_message=warning_message, + when=when, + item=item, + location=None, + ) ) ihook.pytest_warning_recorded.call_historic( kwargs=dict( From be1a2e440e8e3d83f411478ae802f4b1193ba784 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 2 Jun 2020 14:19:18 -0300 Subject: [PATCH 292/823] Merge pull request #7301 from pytest-dev/release-5.4.3 Prepare release 5.4.3 --- doc/en/announce/index.rst | 1 + doc/en/announce/release-5.4.3.rst | 21 +++++++++++++++++++++ doc/en/changelog.rst | 23 +++++++++++++++++++++++ doc/en/example/parametrize.rst | 7 +++---- 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 doc/en/announce/release-5.4.3.rst diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index eeea782743d..4405e6fe04b 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.4.3 release-5.4.2 release-5.4.1 release-5.4.0 diff --git a/doc/en/announce/release-5.4.3.rst b/doc/en/announce/release-5.4.3.rst new file mode 100644 index 00000000000..4d48fc1193f --- /dev/null +++ b/doc/en/announce/release-5.4.3.rst @@ -0,0 +1,21 @@ +pytest-5.4.3 +======================================= + +pytest 5.4.3 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Ran Benita +* Tor Colvin + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 705bb10445b..1a298072b0f 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,29 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.4.3 (2020-06-02) +========================= + +Bug Fixes +--------- + +- `#6428 `_: Paths appearing in error messages are now correct in case the current working directory has + changed since the start of the session. + + +- `#6755 `_: Support deleting paths longer than 260 characters on windows created inside tmpdir. + + +- `#6956 `_: Prevent pytest from printing ConftestImportFailure traceback to stdout. + + +- `#7150 `_: Prevent hiding the underlying exception when ``ConfTestImportFailure`` is raised. + + +- `#7215 `_: Fix regression where running with ``--pdb`` would call the ``tearDown`` methods of ``unittest.TestCase`` + subclasses for skipped tests. + + pytest 5.4.2 (2020-05-08) ========================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 02fd9900438..9500af0d343 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -482,11 +482,10 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ssssssssssss...ssssssssssss [100%] + ssssssssssss......sss...... [100%] ========================= short test summary info ========================== - SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found - SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.7' not found - 3 passed, 24 skipped in 0.12s + SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found + 12 passed, 15 skipped in 0.12s Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- From 8ac18bbecb2eb9646598b4bb1bfe429d4d26cc9d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 2 Jun 2020 16:01:47 -0300 Subject: [PATCH 293/823] Show invalid ini keys sorted Otherwise this relies on the dictionary order of `config.inicfg`, which is insertion order in py36+ but "random" order in py35. --- src/_pytest/config/__init__.py | 2 +- testing/test_config.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 5fc23716dc3..343cdd960ff 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1074,7 +1074,7 @@ def _checkversion(self): ) def _validatekeys(self): - for key in self._get_unknown_ini_keys(): + for key in sorted(self._get_unknown_ini_keys()): message = "Unknown config ini key: {}\n".format(key) if self.known_args_namespace.strict_config: fail(message, pytrace=False) diff --git a/testing/test_config.py b/testing/test_config.py index 6a08e93f39c..c102202edd8 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -158,10 +158,10 @@ def test_confcutdir(self, testdir): """, ["unknown_ini", "another_unknown_ini"], [ - "WARNING: Unknown config ini key: unknown_ini", "WARNING: Unknown config ini key: another_unknown_ini", + "WARNING: Unknown config ini key: unknown_ini", ], - "Unknown config ini key: unknown_ini", + "Unknown config ini key: another_unknown_ini", ), ( """ @@ -200,9 +200,7 @@ def test_invalid_ini_keys( ): testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) config = testdir.parseconfig() - assert config._get_unknown_ini_keys() == invalid_keys, str( - config._get_unknown_ini_keys() - ) + assert sorted(config._get_unknown_ini_keys()) == sorted(invalid_keys) result = testdir.runpytest() result.stderr.fnmatch_lines(stderr_output) From 8cca0238406fc2c34c50ea44f45fdf5fbc36efa4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 2 Jun 2020 12:30:10 -0700 Subject: [PATCH 294/823] cache the pre-commit environment --- .github/workflows/main.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 262ed59463b..056c8d3dbd9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,7 +39,6 @@ jobs: "macos-py37", "macos-py38", - "linting", "docs", "doctesting", ] @@ -112,10 +111,6 @@ jobs: tox_env: "py38-xdist" use_coverage: true - - name: "linting" - python: "3.7" - os: ubuntu-latest - tox_env: "linting" - name: "docs" python: "3.7" os: ubuntu-latest @@ -168,6 +163,20 @@ jobs: CODECOV_NAME: ${{ matrix.name }} run: bash scripts/report-coverage.sh -F GHA,${{ runner.os }} + linting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: set PY + run: echo "::set-env name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" + - uses: actions/cache@v1 + with: + path: ~/.cache/pre-commit + key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} + - run: pip install tox + - run: tox -e linting + deploy: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest' From 2e219ad4f32a3533cce53b865fb7f6548478de66 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 3 Jun 2020 21:51:55 +0300 Subject: [PATCH 295/823] testing: change a test to not use deprecated pluggy __multicall__ protocol It is slated to be removed in pluggy 1.0. --- testing/test_runner.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/test_runner.py b/testing/test_runner.py index 7b0b27a4b0b..32620801d53 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -506,9 +506,10 @@ def pytest_runtest_setup(self, item): @pytest.fixture def mylist(self, request): return request.function.mylist - def pytest_runtest_call(self, item, __multicall__): + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_call(self, item): try: - __multicall__.execute() + (yield).get_result() except ValueError: pass def test_hello1(self, mylist): From 789eea246425fefebf60f188f9a4b4606ff65514 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 4 Jun 2020 09:58:28 -0700 Subject: [PATCH 296/823] Run setup-py-upgrade and setup-cfg-fmt - also ran `pre-commit autoupdate` - https://github.com/asottile/setup-py-upgrade - https://github.com/asottile/setup-cfg-fmt --- .pre-commit-config.yaml | 18 ++++++++---- setup.cfg | 63 +++++++++++++++++++++++++++++------------ setup.py | 35 +---------------------- 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81e30ecc1e4..4f379968e26 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,12 +5,12 @@ repos: - id: black args: [--safe, --quiet] - repo: https://github.com/asottile/blacken-docs - rev: v1.6.0 + rev: v1.7.0 hooks: - id: blacken-docs additional_dependencies: [black==19.10b0] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.5.0 + rev: v3.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -21,23 +21,29 @@ repos: exclude: _pytest/debugging.py language_version: python3 - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.1 + rev: 3.8.2 hooks: - id: flake8 language_version: python3 additional_dependencies: [flake8-typing-imports==1.9.0] - repo: https://github.com/asottile/reorder_python_imports - rev: v1.4.0 + rev: v2.3.0 hooks: - id: reorder-python-imports args: ['--application-directories=.:src', --py3-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.2.1 + rev: v2.4.4 hooks: - id: pyupgrade args: [--py3-plus] +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v1.9.0 + hooks: + - id: setup-cfg-fmt + # TODO: when upgrading setup-cfg-fmt this can be removed + args: [--max-py-version=3.9] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.770 # NOTE: keep this in sync with setup.py. + rev: v0.780 # NOTE: keep this in sync with setup.cfg. hooks: - id: mypy files: ^(src/|testing/) diff --git a/setup.cfg b/setup.cfg index ab3e0f88c7c..a7dd6d1c310 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,35 +2,35 @@ name = pytest description = pytest: simple powerful testing with Python long_description = file: README.rst +long_description_content_type = text/x-rst url = https://docs.pytest.org/en/latest/ -project_urls = - Source=https://github.com/pytest-dev/pytest - Tracker=https://github.com/pytest-dev/pytest/issues - author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others - -license = MIT license -keywords = test, unittest +license = MIT +license_file = LICENSE +platforms = unix, linux, osx, cygwin, win32 classifiers = Development Status :: 6 - Mature Intended Audience :: Developers License :: OSI Approved :: MIT License - Operating System :: POSIX - Operating System :: Microsoft :: Windows Operating System :: MacOS :: MacOS X - Topic :: Software Development :: Testing - Topic :: Software Development :: Libraries - Topic :: Utilities + Operating System :: Microsoft :: Windows + Operating System :: POSIX + Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 -platforms = unix, linux, osx, cygwin, win32 + Topic :: Software Development :: Libraries + Topic :: Software Development :: Testing + Topic :: Utilities +keywords = test, unittest +project_urls = + Source=https://github.com/pytest-dev/pytest + Tracker=https://github.com/pytest-dev/pytest/issues [options] -zip_safe = no packages = _pytest _pytest._code @@ -39,13 +39,40 @@ packages = _pytest.config _pytest.mark pytest - +install_requires = + attrs>=17.4.0 + iniconfig + more-itertools>=4.0.0 + packaging + pluggy>=0.12,<1.0 + py>=1.5.0 + atomicwrites>=1.0;sys_platform=="win32" + colorama;sys_platform=="win32" + importlib-metadata>=0.12;python_version<"3.8" + pathlib2>=2.2.0;python_version<"3.6" python_requires = >=3.5 +package_dir = + =src +setup_requires = + setuptools>=40.0 + setuptools-scm +zip_safe = no [options.entry_points] console_scripts = - pytest=pytest:console_main - py.test=pytest:console_main + pytest=pytest:console_main + py.test=pytest:console_main + +[options.extras_require] +checkqa-mypy = + mypy==0.780 +testing = + argcomplete + hypothesis>=3.56 + mock + nose + requests + xmlschema [build_sphinx] source-dir = doc/en/ @@ -57,7 +84,7 @@ upload-dir = doc/en/build/html [check-manifest] ignore = - src/_pytest/_version.py + src/_pytest/_version.py [devpi:upload] formats = sdist.tgz,bdist_wheel diff --git a/setup.py b/setup.py index 79fef1f4dda..4475e30a71e 100644 --- a/setup.py +++ b/setup.py @@ -1,41 +1,8 @@ from setuptools import setup -# TODO: if py gets upgrade to >=1.6, -# remove _width_of_current_line in terminal.py -INSTALL_REQUIRES = [ - "py>=1.5.0", - "packaging", - "attrs>=17.4.0", # should match oldattrs tox env. - "more-itertools>=4.0.0", - 'atomicwrites>=1.0;sys_platform=="win32"', - 'pathlib2>=2.2.0;python_version<"3.6"', - 'colorama;sys_platform=="win32"', - "pluggy>=0.12,<1.0", - 'importlib-metadata>=0.12;python_version<"3.8"', - "iniconfig", -] - def main(): - setup( - use_scm_version={"write_to": "src/_pytest/_version.py"}, - setup_requires=["setuptools-scm", "setuptools>=40.0"], - package_dir={"": "src"}, - extras_require={ - "testing": [ - "argcomplete", - "hypothesis>=3.56", - "mock", - "nose", - "requests", - "xmlschema", - ], - "checkqa-mypy": [ - "mypy==v0.770", # keep this in sync with .pre-commit-config.yaml. - ], - }, - install_requires=INSTALL_REQUIRES, - ) + setup(use_scm_version={"write_to": "src/_pytest/_version.py"}) if __name__ == "__main__": From 43fa1ee8f9e865319758617d6a1e15bf7eef972f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:15 +0300 Subject: [PATCH 297/823] Type annotate some misc places with no particular connection --- src/_pytest/config/__init__.py | 25 +++++++++++++------------ src/_pytest/mark/__init__.py | 6 +++--- src/_pytest/mark/structures.py | 6 +++--- src/_pytest/nodes.py | 3 ++- src/_pytest/runner.py | 20 ++++++++++---------- 5 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 343cdd960ff..2da7e33aa8f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -14,6 +14,7 @@ from typing import Any from typing import Callable from typing import Dict +from typing import IO from typing import List from typing import Optional from typing import Sequence @@ -295,7 +296,7 @@ class PytestPluginManager(PluginManager): * ``conftest.py`` loading during start-up; """ - def __init__(self): + def __init__(self) -> None: import _pytest.assertion super().__init__("pytest") @@ -315,7 +316,7 @@ def __init__(self): self.add_hookspecs(_pytest.hookspec) self.register(self) if os.environ.get("PYTEST_DEBUG"): - err = sys.stderr + err = sys.stderr # type: IO[str] encoding = getattr(err, "encoding", "utf8") try: err = open( @@ -377,7 +378,7 @@ def parse_hookspec_opts(self, module_or_class, name): } return opts - def register(self, plugin, name=None): + def register(self, plugin: _PluggyPlugin, name: Optional[str] = None): if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: warnings.warn( PytestConfigWarning( @@ -552,7 +553,7 @@ def _check_non_top_pytest_plugins(self, mod, conftestpath): # # - def consider_preparse(self, args, *, exclude_only=False): + def consider_preparse(self, args, *, exclude_only: bool = False) -> None: i = 0 n = len(args) while i < n: @@ -573,7 +574,7 @@ def consider_preparse(self, args, *, exclude_only=False): continue self.consider_pluginarg(parg) - def consider_pluginarg(self, arg): + def consider_pluginarg(self, arg) -> None: if arg.startswith("no:"): name = arg[3:] if name in essential_plugins: @@ -598,13 +599,13 @@ def consider_pluginarg(self, arg): del self._name2plugin["pytest_" + name] self.import_plugin(arg, consider_entry_points=True) - def consider_conftest(self, conftestmodule): + def consider_conftest(self, conftestmodule) -> None: self.register(conftestmodule, name=conftestmodule.__file__) - def consider_env(self): + def consider_env(self) -> None: self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) - def consider_module(self, mod): + def consider_module(self, mod: types.ModuleType) -> None: self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) def _import_plugin_specs(self, spec): @@ -612,7 +613,7 @@ def _import_plugin_specs(self, spec): for import_spec in plugins: self.import_plugin(import_spec) - def import_plugin(self, modname, consider_entry_points=False): + def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None: """ Imports a plugin with ``modname``. If ``consider_entry_points`` is True, entry point names are also considered to find a plugin. @@ -843,19 +844,19 @@ def invocation_dir(self): """Backward compatibility""" return py.path.local(str(self.invocation_params.dir)) - def add_cleanup(self, func): + def add_cleanup(self, func) -> None: """ Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" self._cleanup.append(func) - def _do_configure(self): + def _do_configure(self) -> None: assert not self._configured self._configured = True with warnings.catch_warnings(): warnings.simplefilter("default") self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) - def _ensure_unconfigure(self): + def _ensure_unconfigure(self) -> None: if self._configured: self._configured = False self.hook.pytest_unconfigure(config=self) diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 1cd6e74c939..285c7336b99 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -162,7 +162,7 @@ def __call__(self, subname: str) -> bool: return False -def deselect_by_keyword(items, config): +def deselect_by_keyword(items, config: Config) -> None: keywordexpr = config.option.keyword.lstrip() if not keywordexpr: return @@ -218,7 +218,7 @@ def __call__(self, name: str) -> bool: return name in self.own_mark_names -def deselect_by_mark(items, config): +def deselect_by_mark(items, config: Config) -> None: matchexpr = config.option.markexpr if not matchexpr: return @@ -243,7 +243,7 @@ def deselect_by_mark(items, config): items[:] = remaining -def pytest_collection_modifyitems(items, config): +def pytest_collection_modifyitems(items, config: Config) -> None: deselect_by_keyword(items, config) deselect_by_mark(items, config) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index a34a0c28d05..eb6340e42c3 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -271,7 +271,7 @@ def __call__(self, *args: object, **kwargs: object): return self.with_args(*args, **kwargs) -def get_unpacked_marks(obj): +def get_unpacked_marks(obj) -> List[Mark]: """ obtain the unpacked marks that are stored on an object """ @@ -400,8 +400,8 @@ def _seen(self): seen.update(self.parent.keywords) return seen - def __len__(self): + def __len__(self) -> int: return len(self._seen()) - def __repr__(self): + def __repr__(self) -> str: return "".format(self.node) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 7a8c28cd4fd..df1c79dac4e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -2,6 +2,7 @@ import warnings from functools import lru_cache from typing import Any +from typing import Callable from typing import Dict from typing import List from typing import Optional @@ -312,7 +313,7 @@ def listextrakeywords(self): def listnames(self): return [x.name for x in self.listchain()] - def addfinalizer(self, fin): + def addfinalizer(self, fin: Callable[[], object]) -> None: """ register a function to be called when this node is finalized. This method can only be called when this node is active diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index aa8a5aa8b42..dec6db78858 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -321,9 +321,9 @@ class SetupState: def __init__(self): self.stack = [] # type: List[Node] - self._finalizers = {} # type: Dict[Node, List[Callable[[], None]]] + self._finalizers = {} # type: Dict[Node, List[Callable[[], object]]] - def addfinalizer(self, finalizer, colitem): + def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None: """ attach a finalizer to the given colitem. """ assert colitem and not isinstance(colitem, tuple) assert callable(finalizer) @@ -334,7 +334,7 @@ def _pop_and_teardown(self): colitem = self.stack.pop() self._teardown_with_finalization(colitem) - def _callfinalizers(self, colitem): + def _callfinalizers(self, colitem) -> None: finalizers = self._finalizers.pop(colitem, None) exc = None while finalizers: @@ -349,24 +349,24 @@ def _callfinalizers(self, colitem): if exc: raise exc - def _teardown_with_finalization(self, colitem): + def _teardown_with_finalization(self, colitem) -> None: self._callfinalizers(colitem) colitem.teardown() for colitem in self._finalizers: assert colitem in self.stack - def teardown_all(self): + def teardown_all(self) -> None: while self.stack: self._pop_and_teardown() for key in list(self._finalizers): self._teardown_with_finalization(key) assert not self._finalizers - def teardown_exact(self, item, nextitem): + def teardown_exact(self, item, nextitem) -> None: needed_collectors = nextitem and nextitem.listchain() or [] self._teardown_towards(needed_collectors) - def _teardown_towards(self, needed_collectors): + def _teardown_towards(self, needed_collectors) -> None: exc = None while self.stack: if self.stack == needed_collectors[: len(self.stack)]: @@ -381,7 +381,7 @@ def _teardown_towards(self, needed_collectors): if exc: raise exc - def prepare(self, colitem): + def prepare(self, colitem) -> None: """ setup objects along the collector chain to the test-method and teardown previously setup objects.""" needed_collectors = colitem.listchain() @@ -390,14 +390,14 @@ def prepare(self, colitem): # check if the last collection node has raised an error for col in self.stack: if hasattr(col, "_prepare_exc"): - exc = col._prepare_exc + exc = col._prepare_exc # type: ignore[attr-defined] # noqa: F821 raise exc for col in needed_collectors[len(self.stack) :]: self.stack.append(col) try: col.setup() except TEST_OUTCOME as e: - col._prepare_exc = e + col._prepare_exc = e # type: ignore[attr-defined] # noqa: F821 raise e From ff8b7884e8f1019f60f270eab2c4909ff557dd4e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:15 +0300 Subject: [PATCH 298/823] Type annotate ParameterSet --- src/_pytest/compat.py | 10 +++- src/_pytest/mark/__init__.py | 10 +++- src/_pytest/mark/structures.py | 83 +++++++++++++++++++++++++++------- testing/test_doctest.py | 2 +- 4 files changed, 85 insertions(+), 20 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 4cc22ba4a0d..84f9609a7db 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -1,6 +1,7 @@ """ python version compatibility code """ +import enum import functools import inspect import os @@ -33,13 +34,20 @@ if TYPE_CHECKING: from typing import Type + from typing_extensions import Final _T = TypeVar("_T") _S = TypeVar("_S") -NOTSET = object() +# fmt: off +# Singleton type for NOTSET, as described in: +# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions +class NotSetType(enum.Enum): + token = 0 +NOTSET = NotSetType.token # type: Final # noqa: E305 +# fmt: on MODULE_NOT_FOUND_ERROR = ( "ModuleNotFoundError" if sys.version_info[:2] >= (3, 6) else "ImportError" diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 285c7336b99..c23a38c761e 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -1,7 +1,9 @@ """ generic mechanism for marking and selecting python functions. """ +import typing import warnings from typing import AbstractSet from typing import Optional +from typing import Union import attr @@ -31,7 +33,11 @@ old_mark_config_key = StoreKey[Optional[Config]]() -def param(*values, **kw): +def param( + *values: object, + marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (), + id: Optional[str] = None +) -> ParameterSet: """Specify a parameter in `pytest.mark.parametrize`_ calls or :ref:`parametrized fixtures `. @@ -48,7 +54,7 @@ def test_eval(test_input, expected): :keyword marks: a single mark or a list of marks to be applied to this parameter set. :keyword str id: the id to attribute to this parameter set. """ - return ParameterSet.param(*values, **kw) + return ParameterSet.param(*values, marks=marks, id=id) def pytest_addoption(parser): diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index eb6340e42c3..bfefe7a254d 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -1,11 +1,12 @@ +import collections.abc import inspect +import typing import warnings -from collections import namedtuple -from collections.abc import MutableMapping from typing import Any from typing import Iterable from typing import List from typing import Mapping +from typing import NamedTuple from typing import Optional from typing import Sequence from typing import Set @@ -17,20 +18,29 @@ from .._code import getfslineno from ..compat import ascii_escaped from ..compat import NOTSET +from ..compat import NotSetType +from ..compat import TYPE_CHECKING +from _pytest.config import Config from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning +if TYPE_CHECKING: + from _pytest.python import FunctionDefinition + + EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" -def istestfunc(func): +def istestfunc(func) -> bool: return ( hasattr(func, "__call__") and getattr(func, "__name__", "") != "" ) -def get_empty_parameterset_mark(config, argnames, func): +def get_empty_parameterset_mark( + config: Config, argnames: Sequence[str], func +) -> "MarkDecorator": from ..nodes import Collector requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) @@ -53,16 +63,33 @@ def get_empty_parameterset_mark(config, argnames, func): fs, lineno, ) - return mark(reason=reason) - - -class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): + # Type ignored because MarkDecorator.__call__() is a bit tough to + # annotate ATM. + return mark(reason=reason) # type: ignore[no-any-return] # noqa: F723 + + +class ParameterSet( + NamedTuple( + "ParameterSet", + [ + ("values", Sequence[Union[object, NotSetType]]), + ("marks", "typing.Collection[Union[MarkDecorator, Mark]]"), + ("id", Optional[str]), + ], + ) +): @classmethod - def param(cls, *values, marks=(), id=None): + def param( + cls, + *values: object, + marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (), + id: Optional[str] = None + ) -> "ParameterSet": if isinstance(marks, MarkDecorator): marks = (marks,) else: - assert isinstance(marks, (tuple, list, set)) + # TODO(py36): Change to collections.abc.Collection. + assert isinstance(marks, (collections.abc.Sequence, set)) if id is not None: if not isinstance(id, str): @@ -73,7 +100,11 @@ def param(cls, *values, marks=(), id=None): return cls(values, marks, id) @classmethod - def extract_from(cls, parameterset, force_tuple=False): + def extract_from( + cls, + parameterset: Union["ParameterSet", Sequence[object], object], + force_tuple: bool = False, + ) -> "ParameterSet": """ :param parameterset: a legacy style parameterset that may or may not be a tuple, @@ -89,10 +120,20 @@ def extract_from(cls, parameterset, force_tuple=False): if force_tuple: return cls.param(parameterset) else: - return cls(parameterset, marks=[], id=None) + # TODO: Refactor to fix this type-ignore. Currently the following + # type-checks but crashes: + # + # @pytest.mark.parametrize(('x', 'y'), [1, 2]) + # def test_foo(x, y): pass + return cls(parameterset, marks=[], id=None) # type: ignore[arg-type] # noqa: F821 @staticmethod - def _parse_parametrize_args(argnames, argvalues, *args, **kwargs): + def _parse_parametrize_args( + argnames: Union[str, List[str], Tuple[str, ...]], + argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], + *args, + **kwargs + ) -> Tuple[Union[List[str], Tuple[str, ...]], bool]: if not isinstance(argnames, (tuple, list)): argnames = [x.strip() for x in argnames.split(",") if x.strip()] force_tuple = len(argnames) == 1 @@ -101,13 +142,23 @@ def _parse_parametrize_args(argnames, argvalues, *args, **kwargs): return argnames, force_tuple @staticmethod - def _parse_parametrize_parameters(argvalues, force_tuple): + def _parse_parametrize_parameters( + argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], + force_tuple: bool, + ) -> List["ParameterSet"]: return [ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues ] @classmethod - def _for_parametrize(cls, argnames, argvalues, func, config, function_definition): + def _for_parametrize( + cls, + argnames: Union[str, List[str], Tuple[str, ...]], + argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], + func, + config: Config, + function_definition: "FunctionDefinition", + ) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]: argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) del argvalues @@ -370,7 +421,7 @@ def __getattr__(self, name: str) -> MarkDecorator: MARK_GEN = MarkGenerator() -class NodeKeywords(MutableMapping): +class NodeKeywords(collections.abc.MutableMapping): def __init__(self, node): self.node = node self.parent = node.parent diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 39afb4e9899..c3ba60deb04 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1051,7 +1051,7 @@ def test_number_precision(self, testdir, config_mode): ("1e3", "999"), # The current implementation doesn't understand that numbers inside # strings shouldn't be treated as numbers: - pytest.param("'3.1416'", "'3.14'", marks=pytest.mark.xfail), + pytest.param("'3.1416'", "'3.14'", marks=pytest.mark.xfail), # type: ignore ], ) def test_number_non_matches(self, testdir, expression, output): From 0fb081aec6cd8ed95882d6e63ce93bd7ee4ba6ae Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:15 +0300 Subject: [PATCH 299/823] Type annotate some hookspecs & impls Annotate some "easy" arguments of hooks that repeat in a lot of internal plugins. Not all of the arguments are annotated fully for now. --- src/_pytest/assertion/__init__.py | 5 ++- src/_pytest/cacheprovider.py | 14 +++++-- src/_pytest/capture.py | 3 +- src/_pytest/config/__init__.py | 6 ++- src/_pytest/debugging.py | 11 ++++-- src/_pytest/doctest.py | 5 ++- src/_pytest/faulthandler.py | 10 +++-- src/_pytest/fixtures.py | 5 ++- src/_pytest/helpconfig.py | 14 +++++-- src/_pytest/hookspec.py | 65 ++++++++++++++++++------------- src/_pytest/junitxml.py | 12 +++--- src/_pytest/logging.py | 11 +++--- src/_pytest/mark/__init__.py | 13 +++++-- src/_pytest/mark/structures.py | 2 +- src/_pytest/nodes.py | 2 +- src/_pytest/pastebin.py | 8 ++-- src/_pytest/pytester.py | 7 ++-- src/_pytest/python.py | 17 +++++--- src/_pytest/resultlog.py | 8 ++-- src/_pytest/runner.py | 9 +++-- src/_pytest/setuponly.py | 11 +++++- src/_pytest/setupplan.py | 12 +++++- src/_pytest/skipping.py | 8 ++-- src/_pytest/stepwise.py | 11 ++++-- src/_pytest/terminal.py | 13 ++++--- src/_pytest/warnings.py | 6 ++- 26 files changed, 185 insertions(+), 103 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index b38c6c00660..e739816777a 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -13,12 +13,13 @@ from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser if TYPE_CHECKING: from _pytest.main import Session -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("debugconfig") group.addoption( "--assert", @@ -167,7 +168,7 @@ def call_assertion_pass_hook(lineno, orig, expl): util._reprcompare, util._assertion_pass = saved_assert_hooks -def pytest_sessionfinish(session): +def pytest_sessionfinish(session: "Session") -> None: assertstate = session.config._store.get(assertstate_key, None) if assertstate: if assertstate.hook is not None: diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 511ee2acfa1..cd43c6cacc6 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -11,6 +11,7 @@ from typing import List from typing import Optional from typing import Set +from typing import Union import attr import py @@ -24,6 +25,8 @@ from _pytest._io import TerminalWriter from _pytest.compat import order_preserving_dict from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser from _pytest.main import Session from _pytest.python import Module @@ -329,11 +332,12 @@ def pytest_collection_modifyitems( else: self._report_status += "not deselecting items." - def pytest_sessionfinish(self, session): + def pytest_sessionfinish(self, session: Session) -> None: config = self.config if config.getoption("cacheshow") or hasattr(config, "slaveinput"): return + assert config.cache is not None saved_lastfailed = config.cache.get("cache/lastfailed", {}) if saved_lastfailed != self.lastfailed: config.cache.set("cache/lastfailed", self.lastfailed) @@ -382,7 +386,7 @@ def pytest_sessionfinish(self) -> None: config.cache.set("cache/nodeids", sorted(self.cached_nodeids)) -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group.addoption( "--lf", @@ -440,16 +444,18 @@ def pytest_addoption(parser): ) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.cacheshow: from _pytest.main import wrap_session return wrap_session(config, cacheshow) + return None @pytest.hookimpl(tryfirst=True) def pytest_configure(config: Config) -> None: - config.cache = Cache.for_config(config) + # Type ignored: pending mechanism to store typed objects scoped to config. + config.cache = Cache.for_config(config) # type: ignore # noqa: F821 config.pluginmanager.register(LFPlugin(config), "lfplugin") config.pluginmanager.register(NFPlugin(config), "nfplugin") diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 64f4b8b92f1..5a0cfff368e 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -16,6 +16,7 @@ import pytest from _pytest.compat import TYPE_CHECKING from _pytest.config import Config +from _pytest.config.argparsing import Parser if TYPE_CHECKING: from typing_extensions import Literal @@ -23,7 +24,7 @@ _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group._addoption( "--capture", diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 2da7e33aa8f..d9abc17b4b7 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -407,7 +407,7 @@ def hasplugin(self, name): """Return True if the plugin with the given name is registered.""" return bool(self.get_plugin(name)) - def pytest_configure(self, config): + def pytest_configure(self, config: "Config") -> None: # XXX now that the pluginmanager exposes hookimpl(tryfirst...) # we should remove tryfirst/trylast as markers config.addinivalue_line( @@ -868,7 +868,9 @@ def _ensure_unconfigure(self) -> None: def get_terminal_writer(self): return self.pluginmanager.get_plugin("terminalreporter")._tw - def pytest_cmdline_parse(self, pluginmanager, args): + def pytest_cmdline_parse( + self, pluginmanager: PytestPluginManager, args: List[str] + ) -> object: try: self.parse(args) except UsageError: diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 26c3095dccd..0085d319719 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -4,8 +4,11 @@ import sys from _pytest import outcomes +from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.config import hookimpl +from _pytest.config import PytestPluginManager +from _pytest.config.argparsing import Parser from _pytest.config.exceptions import UsageError @@ -20,7 +23,7 @@ def _validate_usepdb_cls(value): return (modname, classname) -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group._addoption( "--pdb", @@ -44,7 +47,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: import pdb if config.getvalue("trace"): @@ -74,8 +77,8 @@ def fin(): class pytestPDB: """ Pseudo PDB that defers to the real pdb. """ - _pluginmanager = None - _config = None + _pluginmanager = None # type: PytestPluginManager + _config = None # type: Config _saved = [] # type: list _recursive_debug = 0 _wrapped_pdb_cls = None diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index e1dd9691cc9..50f115cd1c4 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -23,6 +23,7 @@ from _pytest._io import TerminalWriter from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING +from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureRequest from _pytest.outcomes import OutcomeException from _pytest.python_api import approx @@ -52,7 +53,7 @@ CHECKER_CLASS = None # type: Optional[Type[doctest.OutputChecker]] -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: parser.addini( "doctest_optionflags", "option flags for doctests", @@ -102,7 +103,7 @@ def pytest_addoption(parser): ) -def pytest_unconfigure(): +def pytest_unconfigure() -> None: global RUNNER_CLASS RUNNER_CLASS = None diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 32e3e50c9fe..9d777b415ea 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -4,13 +4,15 @@ from typing import TextIO import pytest +from _pytest.config import Config +from _pytest.config.argparsing import Parser from _pytest.store import StoreKey fault_handler_stderr_key = StoreKey[TextIO]() -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: help = ( "Dump the traceback of all threads if a test takes " "more than TIMEOUT seconds to finish." @@ -18,7 +20,7 @@ def pytest_addoption(parser): parser.addini("faulthandler_timeout", help, default=0.0) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: import faulthandler if not faulthandler.is_enabled(): @@ -46,14 +48,14 @@ class FaultHandlerHooks: """Implements hooks that will actually install fault handler before tests execute, as well as correctly handle pdb and internal errors.""" - def pytest_configure(self, config): + def pytest_configure(self, config: Config) -> None: import faulthandler stderr_fd_copy = os.dup(self._get_stderr_fileno()) config._store[fault_handler_stderr_key] = open(stderr_fd_copy, "w") faulthandler.enable(file=config._store[fault_handler_stderr_key]) - def pytest_unconfigure(self, config): + def pytest_unconfigure(self, config: Config) -> None: import faulthandler faulthandler.disable() diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index a1574634a01..4583e70f27c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -29,6 +29,7 @@ from _pytest.compat import order_preserving_dict from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING +from _pytest.config.argparsing import Parser from _pytest.deprecated import FILLFUNCARGS from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS from _pytest.deprecated import FUNCARGNAMES @@ -49,7 +50,7 @@ class PseudoFixtureDef: scope = attr.ib() -def pytest_sessionstart(session: "Session"): +def pytest_sessionstart(session: "Session") -> None: import _pytest.python import _pytest.nodes @@ -1202,7 +1203,7 @@ def test_foo(pytestconfig): return request.config -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: parser.addini( "usefixtures", type="args", diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 402ffae66cf..c2519c8afc7 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -2,11 +2,16 @@ import os import sys from argparse import Action +from typing import Optional +from typing import Union import py import pytest +from _pytest.config import Config +from _pytest.config import ExitCode from _pytest.config import PrintHelp +from _pytest.config.argparsing import Parser class HelpAction(Action): @@ -36,7 +41,7 @@ def __call__(self, parser, namespace, values, option_string=None): raise PrintHelp -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("debugconfig") group.addoption( "--version", @@ -109,7 +114,7 @@ def pytest_cmdline_parse(): undo_tracing = config.pluginmanager.enable_tracing() sys.stderr.write("writing pytestdebug information to %s\n" % path) - def unset_tracing(): + def unset_tracing() -> None: debugfile.close() sys.stderr.write("wrote pytestdebug information to %s\n" % debugfile.name) config.trace.root.setwriter(None) @@ -133,7 +138,7 @@ def showversion(config): sys.stderr.write("pytest {}\n".format(pytest.__version__)) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.version > 0: showversion(config) return 0 @@ -142,9 +147,10 @@ def pytest_cmdline_main(config): showhelp(config) config._ensure_unconfigure() return 0 + return None -def showhelp(config): +def showhelp(config: Config) -> None: import textwrap reporter = config.pluginmanager.get_plugin("terminalreporter") diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index de29a40bfe9..8b4505691d1 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,5 +1,6 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ from typing import Any +from typing import List from typing import Mapping from typing import Optional from typing import Tuple @@ -14,10 +15,14 @@ if TYPE_CHECKING: import warnings from _pytest.config import Config + from _pytest.config import ExitCode + from _pytest.config import PytestPluginManager + from _pytest.config import _PluggyPlugin + from _pytest.config.argparsing import Parser from _pytest.main import Session + from _pytest.python import Metafunc from _pytest.reports import BaseReport - hookspec = HookspecMarker("pytest") # ------------------------------------------------------------------------- @@ -26,7 +31,7 @@ @hookspec(historic=True) -def pytest_addhooks(pluginmanager): +def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: """called at plugin registration time to allow adding new hooks via a call to ``pluginmanager.add_hookspecs(module_or_class, prefix)``. @@ -39,7 +44,9 @@ def pytest_addhooks(pluginmanager): @hookspec(historic=True) -def pytest_plugin_registered(plugin, manager): +def pytest_plugin_registered( + plugin: "_PluggyPlugin", manager: "PytestPluginManager" +) -> None: """ a new pytest plugin got registered. :param plugin: the plugin module or instance @@ -51,7 +58,7 @@ def pytest_plugin_registered(plugin, manager): @hookspec(historic=True) -def pytest_addoption(parser, pluginmanager): +def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None: """register argparse-style options and ini-style config values, called once at the beginning of a test run. @@ -89,7 +96,7 @@ def pytest_addoption(parser, pluginmanager): @hookspec(historic=True) -def pytest_configure(config): +def pytest_configure(config: "Config") -> None: """ Allows plugins and conftest files to perform initial configuration. @@ -113,7 +120,9 @@ def pytest_configure(config): @hookspec(firstresult=True) -def pytest_cmdline_parse(pluginmanager, args): +def pytest_cmdline_parse( + pluginmanager: "PytestPluginManager", args: List[str] +) -> Optional[object]: """return initialized config object, parsing the specified args. Stops at first non-None result, see :ref:`firstresult` @@ -127,7 +136,7 @@ def pytest_cmdline_parse(pluginmanager, args): """ -def pytest_cmdline_preparse(config, args): +def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None: """(**Deprecated**) modify command line arguments before option parsing. This hook is considered deprecated and will be removed in a future pytest version. Consider @@ -142,7 +151,7 @@ def pytest_cmdline_preparse(config, args): @hookspec(firstresult=True) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: "Config") -> "Optional[Union[ExitCode, int]]": """ called for performing the main command line action. The default implementation will invoke the configure hooks and runtest_mainloop. @@ -155,7 +164,9 @@ def pytest_cmdline_main(config): """ -def pytest_load_initial_conftests(early_config, parser, args): +def pytest_load_initial_conftests( + early_config: "Config", parser: "Parser", args: List[str] +) -> None: """ implements the loading of initial conftest files ahead of command line option parsing. @@ -198,7 +209,7 @@ def pytest_collection(session: "Session") -> Optional[Any]: """ -def pytest_collection_modifyitems(session, config, items): +def pytest_collection_modifyitems(session: "Session", config: "Config", items): """ called after collection has been performed, may filter or re-order the items in-place. @@ -208,7 +219,7 @@ def pytest_collection_modifyitems(session, config, items): """ -def pytest_collection_finish(session): +def pytest_collection_finish(session: "Session"): """ called after collection has been performed and modified. :param _pytest.main.Session session: the pytest session object @@ -216,7 +227,7 @@ def pytest_collection_finish(session): @hookspec(firstresult=True) -def pytest_ignore_collect(path, config): +def pytest_ignore_collect(path, config: "Config"): """ return True to prevent considering this path for collection. This hook is consulted for all files and directories prior to calling more specific hooks. @@ -304,12 +315,12 @@ def pytest_pyfunc_call(pyfuncitem): Stops at first non-None result, see :ref:`firstresult` """ -def pytest_generate_tests(metafunc): +def pytest_generate_tests(metafunc: "Metafunc") -> None: """ generate (multiple) parametrized calls to a test function.""" @hookspec(firstresult=True) -def pytest_make_parametrize_id(config, val, argname): +def pytest_make_parametrize_id(config: "Config", val, argname) -> Optional[str]: """Return a user-friendly string representation of the given ``val`` that will be used by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``. The parameter name is available as ``argname``, if required. @@ -328,7 +339,7 @@ def pytest_make_parametrize_id(config, val, argname): @hookspec(firstresult=True) -def pytest_runtestloop(session): +def pytest_runtestloop(session: "Session"): """ called for performing the main runtest loop (after collection finished). @@ -411,7 +422,7 @@ def pytest_runtest_logreport(report): @hookspec(firstresult=True) -def pytest_report_to_serializable(config, report): +def pytest_report_to_serializable(config: "Config", report): """ Serializes the given report object into a data structure suitable for sending over the wire, e.g. converted to JSON. @@ -419,7 +430,7 @@ def pytest_report_to_serializable(config, report): @hookspec(firstresult=True) -def pytest_report_from_serializable(config, data): +def pytest_report_from_serializable(config: "Config", data): """ Restores a report object previously serialized with pytest_report_to_serializable(). """ @@ -456,7 +467,7 @@ def pytest_fixture_post_finalizer(fixturedef, request): # ------------------------------------------------------------------------- -def pytest_sessionstart(session): +def pytest_sessionstart(session: "Session") -> None: """ called after the ``Session`` object has been created and before performing collection and entering the run test loop. @@ -464,7 +475,9 @@ def pytest_sessionstart(session): """ -def pytest_sessionfinish(session, exitstatus): +def pytest_sessionfinish( + session: "Session", exitstatus: "Union[int, ExitCode]" +) -> None: """ called after whole test run finished, right before returning the exit status to the system. :param _pytest.main.Session session: the pytest session object @@ -472,7 +485,7 @@ def pytest_sessionfinish(session, exitstatus): """ -def pytest_unconfigure(config): +def pytest_unconfigure(config: "Config") -> None: """ called before test process is exited. :param _pytest.config.Config config: pytest config object @@ -484,7 +497,7 @@ def pytest_unconfigure(config): # ------------------------------------------------------------------------- -def pytest_assertrepr_compare(config, op, left, right): +def pytest_assertrepr_compare(config: "Config", op, left, right): """return explanation for comparisons in failing assert expressions. Return None for no custom explanation, otherwise return a list @@ -539,7 +552,7 @@ def pytest_assertion_pass(item, lineno, orig, expl): # ------------------------------------------------------------------------- -def pytest_report_header(config, startdir): +def pytest_report_header(config: "Config", startdir): """ return a string or list of strings to be displayed as header info for terminal reporting. :param _pytest.config.Config config: pytest config object @@ -560,7 +573,7 @@ def pytest_report_header(config, startdir): """ -def pytest_report_collectionfinish(config, startdir, items): +def pytest_report_collectionfinish(config: "Config", startdir, items): """ .. versionadded:: 3.2 @@ -610,7 +623,7 @@ def pytest_report_teststatus( """ -def pytest_terminal_summary(terminalreporter, exitstatus, config): +def pytest_terminal_summary(terminalreporter, exitstatus, config: "Config"): """Add a section to terminal summary reporting. :param _pytest.terminal.TerminalReporter terminalreporter: the internal terminal reporter object @@ -723,7 +736,7 @@ def pytest_exception_interact(node, call, report): """ -def pytest_enter_pdb(config, pdb): +def pytest_enter_pdb(config: "Config", pdb): """ called upon pdb.set_trace(), can be used by plugins to take special action just before the python debugger enters in interactive mode. @@ -732,7 +745,7 @@ def pytest_enter_pdb(config, pdb): """ -def pytest_leave_pdb(config, pdb): +def pytest_leave_pdb(config: "Config", pdb): """ called when leaving pdb (e.g. with continue after pdb.set_trace()). Can be used by plugins to take special action just after the python diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index b26112ac1a4..b0790bc794d 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -21,7 +21,9 @@ from _pytest import deprecated from _pytest import nodes from _pytest import timing +from _pytest.config import Config from _pytest.config import filename_arg +from _pytest.config.argparsing import Parser from _pytest.store import StoreKey from _pytest.warnings import _issue_warning_captured @@ -361,7 +363,7 @@ def record_func(name, value): return record_func -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("terminal reporting") group.addoption( "--junitxml", @@ -406,7 +408,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: xmlpath = config.option.xmlpath # prevent opening xmllog on slave nodes (xdist) if xmlpath and not hasattr(config, "slaveinput"): @@ -426,7 +428,7 @@ def pytest_configure(config): config.pluginmanager.register(config._store[xml_key]) -def pytest_unconfigure(config): +def pytest_unconfigure(config: Config) -> None: xml = config._store.get(xml_key, None) if xml: del config._store[xml_key] @@ -624,10 +626,10 @@ def pytest_internalerror(self, excrepr): reporter.attrs.update(classname="pytest", name="internal") reporter._add_simple(Junit.error, "internal error", excrepr) - def pytest_sessionstart(self): + def pytest_sessionstart(self) -> None: self.suite_start_time = timing.time() - def pytest_sessionfinish(self): + def pytest_sessionfinish(self) -> None: dirname = os.path.dirname(os.path.abspath(self.logfile)) if not os.path.isdir(dirname): os.makedirs(dirname) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index f6a2063271f..b832f6994fb 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -19,6 +19,7 @@ from _pytest.config import _strtobool from _pytest.config import Config from _pytest.config import create_terminal_writer +from _pytest.config.argparsing import Parser from _pytest.pathlib import Path from _pytest.store import StoreKey @@ -180,7 +181,7 @@ def get_option_ini(config, *names): return ret -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: """Add options to control log capturing.""" group = parser.getgroup("logging") @@ -478,7 +479,7 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i # run after terminalreporter/capturemanager are configured @pytest.hookimpl(trylast=True) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") @@ -601,7 +602,7 @@ def _log_cli_enabled(self): return True @pytest.hookimpl(hookwrapper=True, tryfirst=True) - def pytest_sessionstart(self): + 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): @@ -679,7 +680,7 @@ def pytest_runtest_logfinish(self): self.log_cli_handler.set_when("finish") @pytest.hookimpl(hookwrapper=True, tryfirst=True) - def pytest_sessionfinish(self): + 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): @@ -687,7 +688,7 @@ def pytest_sessionfinish(self): yield @pytest.hookimpl - def pytest_unconfigure(self): + def pytest_unconfigure(self) -> None: # Close the FileHandler explicitly. # (logging.shutdown might have lost the weakref?!) self.log_file_handler.close() diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index c23a38c761e..05afb7749ac 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -18,8 +18,10 @@ from .structures import ParameterSet from _pytest.compat import TYPE_CHECKING from _pytest.config import Config +from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config import UsageError +from _pytest.config.argparsing import Parser from _pytest.deprecated import MINUS_K_COLON from _pytest.deprecated import MINUS_K_DASH from _pytest.store import StoreKey @@ -27,6 +29,7 @@ if TYPE_CHECKING: from _pytest.nodes import Item + __all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"] @@ -57,7 +60,7 @@ def test_eval(test_input, expected): return ParameterSet.param(*values, marks=marks, id=id) -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group._addoption( "-k", @@ -100,7 +103,7 @@ def pytest_addoption(parser): @hookimpl(tryfirst=True) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: import _pytest.config if config.option.markers: @@ -116,6 +119,8 @@ def pytest_cmdline_main(config): config._ensure_unconfigure() return 0 + return None + @attr.s(slots=True) class KeywordMatcher: @@ -254,7 +259,7 @@ def pytest_collection_modifyitems(items, config: Config) -> None: deselect_by_mark(items, config) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: config._store[old_mark_config_key] = MARK_GEN._config MARK_GEN._config = config @@ -267,5 +272,5 @@ def pytest_configure(config): ) -def pytest_unconfigure(config): +def pytest_unconfigure(config: Config) -> None: MARK_GEN._config = config._store.get(old_mark_config_key, None) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index bfefe7a254d..7ae7d5d4fb1 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -374,7 +374,7 @@ def test_function(): applies a 'slowtest' :class:`Mark` on ``test_function``. """ - _config = None + _config = None # type: Optional[Config] _markers = set() # type: Set[str] def __getattr__(self, name: str) -> MarkDecorator: diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index df1c79dac4e..c9b633579e8 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -123,7 +123,7 @@ def __init__( #: the pytest config object if config: - self.config = config + self.config = config # type: Config else: if not parent: raise TypeError("config or parent must be provided") diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index cbaa9a9f5f1..091d3f81762 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -4,13 +4,15 @@ from typing import IO import pytest +from _pytest.config import Config +from _pytest.config.argparsing import Parser from _pytest.store import StoreKey pastebinfile_key = StoreKey[IO[bytes]]() -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("terminal reporting") group._addoption( "--pastebin", @@ -24,7 +26,7 @@ def pytest_addoption(parser): @pytest.hookimpl(trylast=True) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: if config.option.pastebin == "all": tr = config.pluginmanager.getplugin("terminalreporter") # if no terminal reporter plugin is present, nothing we can do here; @@ -44,7 +46,7 @@ def tee_write(s, **kwargs): tr._tw.write = tee_write -def pytest_unconfigure(config): +def pytest_unconfigure(config: Config) -> None: if pastebinfile_key in config._store: pastebinfile = config._store[pastebinfile_key] # get terminal contents and delete file diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 3c81dd759bc..ae7bdcec8e8 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -31,6 +31,7 @@ from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch @@ -53,7 +54,7 @@ ] -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: parser.addoption( "--lsof", action="store_true", @@ -78,7 +79,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: if config.getvalue("lsof"): checker = LsofFdLeakChecker() if checker.matching_platform(): @@ -938,7 +939,7 @@ def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): rec = [] class Collect: - def pytest_configure(x, config): + def pytest_configure(x, config: Config) -> None: rec.append(self.make_hook_recorder(config.pluginmanager)) plugins.append(Collect()) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 76fccb4a18d..45d3384df83 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -16,6 +16,7 @@ from typing import Iterable from typing import List from typing import Optional +from typing import Set from typing import Tuple from typing import Union @@ -42,9 +43,12 @@ from _pytest.compat import safe_isclass from _pytest.compat import STRING_TYPES 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 FUNCARGNAMES from _pytest.fixtures import FuncFixtureInfo +from _pytest.main import Session from _pytest.mark import MARK_GEN from _pytest.mark import ParameterSet from _pytest.mark.structures import get_unpacked_marks @@ -57,7 +61,7 @@ from _pytest.warning_types import PytestUnhandledCoroutineWarning -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group.addoption( "--fixtures", @@ -112,13 +116,14 @@ def pytest_addoption(parser): ) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.showfixtures: showfixtures(config) return 0 if config.option.show_fixtures_per_test: show_fixtures_per_test(config) return 0 + return None def pytest_generate_tests(metafunc: "Metafunc") -> None: @@ -127,7 +132,7 @@ def pytest_generate_tests(metafunc: "Metafunc") -> None: metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) # type: ignore[misc] -def pytest_configure(config): +def pytest_configure(config: Config) -> None: config.addinivalue_line( "markers", "parametrize(argnames, argvalues): call a test function multiple " @@ -1308,13 +1313,13 @@ def write_item(item): write_item(session_item) -def showfixtures(config): +def showfixtures(config: Config) -> Union[int, ExitCode]: from _pytest.main import wrap_session return wrap_session(config, _showfixtures_main) -def _showfixtures_main(config, session): +def _showfixtures_main(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() @@ -1325,7 +1330,7 @@ def _showfixtures_main(config, session): fm = session._fixturemanager available = [] - seen = set() + seen = set() # type: Set[Tuple[str, str]] for argname, fixturedefs in fm._arg2fixturedefs.items(): assert fixturedefs is not None diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index 3cfa9e0e96a..acc89afe2b7 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -5,13 +5,15 @@ import py +from _pytest.config import Config +from _pytest.config.argparsing import Parser from _pytest.store import StoreKey resultlog_key = StoreKey["ResultLog"]() -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("terminal reporting", "resultlog plugin options") group.addoption( "--resultlog", @@ -23,7 +25,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: resultlog = config.option.resultlog # prevent opening resultlog on slave nodes (xdist) if resultlog and not hasattr(config, "slaveinput"): @@ -40,7 +42,7 @@ def pytest_configure(config): _issue_warning_captured(RESULT_LOG, config.hook, stacklevel=2) -def pytest_unconfigure(config): +def pytest_unconfigure(config: Config) -> None: resultlog = config._store.get(resultlog_key, None) if resultlog: resultlog.logfile.close() diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index dec6db78858..c7f6d8811ab 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -17,6 +17,7 @@ from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest.compat import TYPE_CHECKING +from _pytest.config.argparsing import Parser from _pytest.nodes import Collector from _pytest.nodes import Node from _pytest.outcomes import Exit @@ -27,11 +28,13 @@ from typing import Type from typing_extensions import Literal + from _pytest.main import Session + # # pytest plugin hooks -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("terminal reporting", "reporting", after="general") group.addoption( "--durations", @@ -75,11 +78,11 @@ def pytest_terminal_summary(terminalreporter): tr.write_line("{:02.2f}s {:<8} {}".format(rep.duration, rep.when, rep.nodeid)) -def pytest_sessionstart(session): +def pytest_sessionstart(session: "Session") -> None: session._setupstate = SetupState() -def pytest_sessionfinish(session): +def pytest_sessionfinish(session: "Session") -> None: session._setupstate.teardown_all() diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 9e4cd951947..fe328d519c6 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -1,8 +1,14 @@ +from typing import Optional +from typing import Union + import pytest from _pytest._io.saferepr import saferepr +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("debugconfig") group.addoption( "--setuponly", @@ -76,6 +82,7 @@ def _show_fixture_action(fixturedef, msg): @pytest.hookimpl(tryfirst=True) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.setuponly: config.option.setupshow = True + return None diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 6fdd3aed064..834d4ae2d51 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -1,7 +1,13 @@ +from typing import Optional +from typing import Union + import pytest +from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("debugconfig") group.addoption( "--setupplan", @@ -19,10 +25,12 @@ def pytest_fixture_setup(fixturedef, request): my_cache_key = fixturedef.cache_key(request) fixturedef.cached_result = (None, my_cache_key, None) return fixturedef.cached_result + return None @pytest.hookimpl(tryfirst=True) -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.setupplan: config.option.setuponly = True config.option.setupshow = True + return None diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 62a9ca491e7..5e5fcc080e1 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -1,5 +1,7 @@ """ support for skip/xfail functions and markers. """ +from _pytest.config import Config from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser from _pytest.mark.evaluate import MarkEvaluator from _pytest.outcomes import fail from _pytest.outcomes import skip @@ -12,7 +14,7 @@ unexpectedsuccess_key = StoreKey[str]() -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group.addoption( "--runxfail", @@ -31,7 +33,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: if config.option.runxfail: # yay a hack import pytest @@ -42,7 +44,7 @@ def pytest_configure(config): def nop(*args, **kwargs): pass - nop.Exception = xfail.Exception + nop.Exception = xfail.Exception # type: ignore[attr-defined] # noqa: F821 setattr(pytest, "xfail", nop) config.addinivalue_line( diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 6fa21cd1c65..3cbf0be9fc0 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,7 +1,10 @@ import pytest +from _pytest.config import Config +from _pytest.config.argparsing import Parser +from _pytest.main import Session -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group.addoption( "--sw", @@ -19,7 +22,7 @@ def pytest_addoption(parser): @pytest.hookimpl -def pytest_configure(config): +def pytest_configure(config: Config) -> None: config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") @@ -34,7 +37,7 @@ def __init__(self, config): self.lastfailed = config.cache.get("cache/stepwise", None) self.skip = config.getvalue("stepwise_skip") - def pytest_sessionstart(self, session): + def pytest_sessionstart(self, session: Session) -> None: self.session = session def pytest_collection_modifyitems(self, session, config, items): @@ -100,7 +103,7 @@ def pytest_report_collectionfinish(self): if self.active and self.config.getoption("verbose") >= 0 and self.report_status: return "stepwise: %s" % self.report_status - def pytest_sessionfinish(self, session): + def pytest_sessionfinish(self, session: Session) -> None: if self.active: self.config.cache.set("cache/stepwise", self.lastfailed) else: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index e384e02b204..6f4b96e1e16 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -17,6 +17,7 @@ from typing import Optional from typing import Set from typing import Tuple +from typing import Union import attr import pluggy @@ -29,8 +30,10 @@ from _pytest._io import TerminalWriter from _pytest._io.wcwidth import wcswidth from _pytest.compat import order_preserving_dict +from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.config.argparsing import Parser from _pytest.deprecated import TERMINALWRITER_WRITER from _pytest.main import Session from _pytest.reports import CollectReport @@ -77,7 +80,7 @@ def __call__(self, parser, namespace, values, option_string=None): namespace.quiet = getattr(namespace, "quiet", 0) + 1 -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("terminal reporting", "reporting", after="general") group._addoption( "-v", @@ -423,7 +426,7 @@ def pytest_warning_recorded(self, warning_message, nodeid): ) self._add_stats("warnings", [warning_report]) - def pytest_plugin_registered(self, plugin): + def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: if self.config.option.traceconfig: msg = "PLUGIN registered: {}".format(plugin) # XXX this event may happen during setup/teardown time @@ -717,7 +720,7 @@ def _printcollecteditems(self, items): self._tw.line("{}{}".format(indent + " ", line)) @pytest.hookimpl(hookwrapper=True) - def pytest_sessionfinish(self, session: Session, exitstatus: ExitCode): + def pytest_sessionfinish(self, session: Session, exitstatus: Union[int, ExitCode]): outcome = yield outcome.get_result() self._tw.line("") @@ -752,10 +755,10 @@ def pytest_terminal_summary(self): # Display any extra warnings from teardown here (if any). self.summary_warnings() - def pytest_keyboard_interrupt(self, excinfo): + def pytest_keyboard_interrupt(self, excinfo) -> None: self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) - def pytest_unconfigure(self): + def pytest_unconfigure(self) -> None: if hasattr(self, "_keyboardinterrupt_memo"): self._report_keyboardinterrupt() diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 33d89428be8..83e338cb3d4 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -8,6 +8,8 @@ import pytest from _pytest.compat import TYPE_CHECKING +from _pytest.config import Config +from _pytest.config.argparsing import Parser from _pytest.main import Session if TYPE_CHECKING: @@ -49,7 +51,7 @@ def _parse_filter( return (action, message, category, module, lineno) -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("pytest-warnings") group.addoption( "-W", @@ -66,7 +68,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: config.addinivalue_line( "markers", "filterwarnings(warning): add a warning filter to the given test. " From f8de4242414c06fcd1886afdffd76002280ce4e6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:15 +0300 Subject: [PATCH 300/823] Type annotate CallSpec2 --- src/_pytest/python.py | 55 ++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 45d3384df83..e46d498abd6 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -15,6 +15,7 @@ from typing import Dict from typing import Iterable from typing import List +from typing import Mapping from typing import Optional from typing import Set from typing import Tuple @@ -44,6 +45,7 @@ from _pytest.compat import STRING_TYPES from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.compat import TYPE_CHECKING from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.deprecated import FUNCARGNAMES @@ -53,6 +55,7 @@ from _pytest.mark import ParameterSet from _pytest.mark.structures import get_unpacked_marks from _pytest.mark.structures import Mark +from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail from _pytest.outcomes import skip @@ -60,6 +63,9 @@ from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning +if TYPE_CHECKING: + from typing_extensions import Literal + def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") @@ -772,16 +778,17 @@ def hasnew(obj): class CallSpec2: - def __init__(self, metafunc): + def __init__(self, metafunc: "Metafunc") -> None: self.metafunc = metafunc - self.funcargs = {} - self._idlist = [] - self.params = {} - self._arg2scopenum = {} # used for sorting parametrized resources - self.marks = [] - self.indices = {} - - def copy(self): + self.funcargs = {} # type: Dict[str, object] + self._idlist = [] # type: List[str] + self.params = {} # type: Dict[str, object] + # Used for sorting parametrized resources. + self._arg2scopenum = {} # type: Dict[str, int] + self.marks = [] # type: List[Mark] + self.indices = {} # type: Dict[str, int] + + def copy(self) -> "CallSpec2": cs = CallSpec2(self.metafunc) cs.funcargs.update(self.funcargs) cs.params.update(self.params) @@ -791,25 +798,39 @@ def copy(self): cs._idlist = list(self._idlist) return cs - def _checkargnotcontained(self, arg): + def _checkargnotcontained(self, arg: str) -> None: if arg in self.params or arg in self.funcargs: raise ValueError("duplicate {!r}".format(arg)) - def getparam(self, name): + def getparam(self, name: str) -> object: try: return self.params[name] except KeyError: raise ValueError(name) @property - def id(self): + def id(self) -> str: return "-".join(map(str, self._idlist)) - def setmulti2(self, valtypes, argnames, valset, id, marks, scopenum, param_index): + def setmulti2( + self, + valtypes: "Mapping[str, Literal['params', 'funcargs']]", + argnames: typing.Sequence[str], + valset: Iterable[object], + id: str, + marks: Iterable[Union[Mark, MarkDecorator]], + scopenum: int, + param_index: int, + ) -> None: for arg, val in zip(argnames, valset): self._checkargnotcontained(arg) valtype_for_arg = valtypes[arg] - getattr(self, valtype_for_arg)[arg] = val + if valtype_for_arg == "params": + self.params[arg] = val + elif valtype_for_arg == "funcargs": + self.funcargs[arg] = val + else: # pragma: no cover + assert False, "Unhandled valtype for arg: {}".format(valtype_for_arg) self.indices[arg] = param_index self._arg2scopenum[arg] = scopenum self._idlist.append(id) @@ -1049,7 +1070,7 @@ def _resolve_arg_value_types( self, argnames: typing.Sequence[str], indirect: Union[bool, typing.Sequence[str]], - ) -> Dict[str, str]: + ) -> Dict[str, "Literal['params', 'funcargs']"]: """Resolves if each parametrized argument must be considered a parameter to a fixture or a "funcarg" to the function, based on the ``indirect`` parameter of the parametrized() call. @@ -1061,7 +1082,9 @@ def _resolve_arg_value_types( * "funcargs" if the argname should be a parameter to the parametrized test function. """ if isinstance(indirect, bool): - valtypes = dict.fromkeys(argnames, "params" if indirect else "funcargs") + valtypes = dict.fromkeys( + argnames, "params" if indirect else "funcargs" + ) # type: Dict[str, Literal["params", "funcargs"]] elif isinstance(indirect, Sequence): valtypes = dict.fromkeys(argnames, "funcargs") for arg in indirect: From be00e12d47c820f0a90d24cd76ada8a0366c5a67 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:15 +0300 Subject: [PATCH 301/823] Type annotate main.py and some parts related to collection --- src/_pytest/config/__init__.py | 2 +- src/_pytest/doctest.py | 16 +++-- src/_pytest/hookspec.py | 14 ++++- src/_pytest/main.py | 105 +++++++++++++++++++++++---------- src/_pytest/nodes.py | 18 ++++-- src/_pytest/python.py | 53 +++++++++++------ src/_pytest/reports.py | 11 +++- src/_pytest/runner.py | 4 +- src/_pytest/unittest.py | 27 ++++++--- 9 files changed, 175 insertions(+), 75 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index d9abc17b4b7..ff6aee744e3 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -840,7 +840,7 @@ def __init__( self.cache = None # type: Optional[Cache] @property - def invocation_dir(self): + def invocation_dir(self) -> py.path.local: """Backward compatibility""" return py.path.local(str(self.invocation_params.dir)) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 50f115cd1c4..026476b8aae 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -7,6 +7,7 @@ import warnings from contextlib import contextmanager from typing import Dict +from typing import Iterable from typing import List from typing import Optional from typing import Sequence @@ -109,13 +110,18 @@ def pytest_unconfigure() -> None: RUNNER_CLASS = None -def pytest_collect_file(path: py.path.local, parent): +def pytest_collect_file( + path: py.path.local, parent +) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: config = parent.config if path.ext == ".py": if config.option.doctestmodules and not _is_setup_py(path): - return DoctestModule.from_parent(parent, fspath=path) + mod = DoctestModule.from_parent(parent, fspath=path) # type: DoctestModule + return mod elif _is_doctest(config, path, parent): - return DoctestTextfile.from_parent(parent, fspath=path) + txt = DoctestTextfile.from_parent(parent, fspath=path) # type: DoctestTextfile + return txt + return None def _is_setup_py(path: py.path.local) -> bool: @@ -365,7 +371,7 @@ def _get_continue_on_failure(config): class DoctestTextfile(pytest.Module): obj = None - def collect(self): + def collect(self) -> Iterable[DoctestItem]: import doctest # inspired by doctest.testfile; ideally we would use it directly, @@ -444,7 +450,7 @@ def _mock_aware_unwrap(obj, stop=None): class DoctestModule(pytest.Module): - def collect(self): + def collect(self) -> Iterable[DoctestItem]: import doctest class MockAwareDocTestFinder(doctest.DocTestFinder): diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 8b4505691d1..1321eff540d 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -6,6 +6,7 @@ from typing import Tuple from typing import Union +import py.path from pluggy import HookspecMarker from .deprecated import COLLECT_DIRECTORY_HOOK @@ -20,9 +21,14 @@ from _pytest.config import _PluggyPlugin from _pytest.config.argparsing import Parser from _pytest.main import Session + from _pytest.nodes import Collector + from _pytest.nodes import Item from _pytest.python import Metafunc + from _pytest.python import Module + from _pytest.python import PyCollector from _pytest.reports import BaseReport + hookspec = HookspecMarker("pytest") # ------------------------------------------------------------------------- @@ -249,7 +255,7 @@ def pytest_collect_directory(path, parent): """ -def pytest_collect_file(path, parent): +def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]": """ return collection Node or None for the given path. Any new node needs to have the specified ``parent`` as a parent. @@ -289,7 +295,7 @@ def pytest_make_collect_report(collector): @hookspec(firstresult=True) -def pytest_pycollect_makemodule(path, parent): +def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Optional[Module]": """ return a Module collector or None for the given path. This hook will be called for each matching test module path. The pytest_collect_file hook needs to be used if you want to @@ -302,7 +308,9 @@ def pytest_pycollect_makemodule(path, parent): @hookspec(firstresult=True) -def pytest_pycollect_makeitem(collector, name, obj): +def pytest_pycollect_makeitem( + collector: "PyCollector", name: str, obj +) -> "Union[None, Item, Collector, List[Union[Item, Collector]]]": """ return custom item/collector for a python object in a module, or None. Stops at first non-None result, see :ref:`firstresult` """ diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 4eb47be2cde..a0007d226df 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -7,9 +7,11 @@ from typing import Callable from typing import Dict from typing import FrozenSet +from typing import Iterator from typing import List from typing import Optional from typing import Sequence +from typing import Set from typing import Tuple from typing import Union @@ -18,12 +20,14 @@ import _pytest._code from _pytest import nodes +from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import directory_arg from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config import UsageError +from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit from _pytest.reports import CollectReport @@ -38,7 +42,7 @@ from _pytest.python import Package -def pytest_addoption(parser): +def pytest_addoption(parser: Parser) -> None: parser.addini( "norecursedirs", "directory patterns to avoid for recursion", @@ -241,7 +245,7 @@ def wrap_session( return session.exitstatus -def pytest_cmdline_main(config): +def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]: return wrap_session(config, _main) @@ -258,11 +262,11 @@ def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]: return None -def pytest_collection(session): +def pytest_collection(session: "Session") -> Sequence[nodes.Item]: return session.perform_collect() -def pytest_runtestloop(session): +def pytest_runtestloop(session: "Session") -> bool: if session.testsfailed and not session.config.option.continue_on_collection_errors: raise session.Interrupted( "%d error%s during collection" @@ -282,7 +286,7 @@ def pytest_runtestloop(session): return True -def _in_venv(path): +def _in_venv(path: py.path.local) -> bool: """Attempts to detect if ``path`` is the root of a Virtual Environment by checking for the existence of the appropriate activate script""" bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin") @@ -328,7 +332,7 @@ def pytest_ignore_collect( return None -def pytest_collection_modifyitems(items, config): +def pytest_collection_modifyitems(items, config: Config) -> None: deselect_prefixes = tuple(config.getoption("deselect") or []) if not deselect_prefixes: return @@ -385,8 +389,8 @@ def __init__(self, config: Config) -> None: ) self.testsfailed = 0 self.testscollected = 0 - self.shouldstop = False - self.shouldfail = False + self.shouldstop = False # type: Union[bool, str] + self.shouldfail = False # type: Union[bool, str] self.trace = config.trace.root.get("collection") self.startdir = config.invocation_dir self._initialpaths = frozenset() # type: FrozenSet[py.path.local] @@ -412,10 +416,11 @@ def __init__(self, config: Config) -> None: self.config.pluginmanager.register(self, name="session") @classmethod - def from_config(cls, config): - return cls._create(config) + def from_config(cls, config: Config) -> "Session": + session = cls._create(config) # type: Session + return session - def __repr__(self): + def __repr__(self) -> str: return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % ( self.__class__.__name__, self.name, @@ -429,14 +434,14 @@ def _node_location_to_relpath(self, node_path: py.path.local) -> str: return self._bestrelpathcache[node_path] @hookimpl(tryfirst=True) - def pytest_collectstart(self): + def pytest_collectstart(self) -> None: if self.shouldfail: raise self.Failed(self.shouldfail) if self.shouldstop: raise self.Interrupted(self.shouldstop) @hookimpl(tryfirst=True) - def pytest_runtest_logreport(self, report): + def pytest_runtest_logreport(self, report) -> None: if report.failed and not hasattr(report, "wasxfail"): self.testsfailed += 1 maxfail = self.config.getvalue("maxfail") @@ -445,13 +450,27 @@ def pytest_runtest_logreport(self, report): pytest_collectreport = pytest_runtest_logreport - def isinitpath(self, path): + def isinitpath(self, path: py.path.local) -> bool: return path in self._initialpaths def gethookproxy(self, fspath: py.path.local): return super()._gethookproxy(fspath) - def perform_collect(self, args=None, genitems=True): + @overload + def perform_collect( + self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ... + ) -> Sequence[nodes.Item]: + raise NotImplementedError() + + @overload # noqa: F811 + def perform_collect( # noqa: F811 + self, args: Optional[Sequence[str]] = ..., genitems: bool = ... + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: + raise NotImplementedError() + + def perform_collect( # noqa: F811 + self, args: Optional[Sequence[str]] = None, genitems: bool = True + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: hook = self.config.hook try: items = self._perform_collect(args, genitems) @@ -464,15 +483,29 @@ def perform_collect(self, args=None, genitems=True): self.testscollected = len(items) return items - def _perform_collect(self, args, genitems): + @overload + def _perform_collect( + self, args: Optional[Sequence[str]], genitems: "Literal[True]" + ) -> Sequence[nodes.Item]: + raise NotImplementedError() + + @overload # noqa: F811 + def _perform_collect( # noqa: F811 + self, args: Optional[Sequence[str]], genitems: bool + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: + raise NotImplementedError() + + def _perform_collect( # noqa: F811 + self, args: Optional[Sequence[str]], genitems: bool + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: if args is None: args = self.config.args self.trace("perform_collect", self, args) self.trace.root.indent += 1 - self._notfound = [] + self._notfound = [] # type: List[Tuple[str, NoMatch]] initialpaths = [] # type: List[py.path.local] self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]] - self.items = items = [] + self.items = items = [] # type: List[nodes.Item] for arg in args: fspath, parts = self._parsearg(arg) self._initial_parts.append((fspath, parts)) @@ -495,7 +528,7 @@ def _perform_collect(self, args, genitems): self.items.extend(self.genitems(node)) return items - def collect(self): + def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: for fspath, parts in self._initial_parts: self.trace("processing argument", (fspath, parts)) self.trace.root.indent += 1 @@ -513,7 +546,9 @@ def collect(self): self._collection_node_cache3.clear() self._collection_pkg_roots.clear() - def _collect(self, argpath, names): + def _collect( + self, argpath: py.path.local, names: List[str] + ) -> Iterator[Union[nodes.Item, nodes.Collector]]: from _pytest.python import Package # Start with a Session root, and delve to argpath item (dir or file) @@ -541,7 +576,7 @@ def _collect(self, argpath, names): if argpath.check(dir=1): assert not names, "invalid arg {!r}".format((argpath, names)) - seen_dirs = set() + seen_dirs = set() # type: Set[py.path.local] for path in argpath.visit( fil=self._visit_filter, rec=self._recurse, bf=True, sort=True ): @@ -582,8 +617,9 @@ def _collect(self, argpath, names): # Module itself, so just use that. If this special case isn't taken, then all # the files in the package will be yielded. if argpath.basename == "__init__.py": + assert isinstance(m[0], nodes.Collector) try: - yield next(m[0].collect()) + yield next(iter(m[0].collect())) except StopIteration: # The package collects nothing with only an __init__.py # file in it, which gets ignored by the default @@ -593,10 +629,11 @@ def _collect(self, argpath, names): yield from m @staticmethod - def _visit_filter(f): - return f.check(file=1) + def _visit_filter(f: py.path.local) -> bool: + # TODO: Remove type: ignore once `py` is typed. + return f.check(file=1) # type: ignore - def _tryconvertpyarg(self, x): + def _tryconvertpyarg(self, x: str) -> str: """Convert a dotted module name to path.""" try: spec = importlib.util.find_spec(x) @@ -605,14 +642,14 @@ def _tryconvertpyarg(self, x): # ValueError: not a module name except (AttributeError, ImportError, ValueError): return x - if spec is None or spec.origin in {None, "namespace"}: + if spec is None or spec.origin is None or spec.origin == "namespace": return x elif spec.submodule_search_locations: return os.path.dirname(spec.origin) else: return spec.origin - def _parsearg(self, arg): + def _parsearg(self, arg: str) -> Tuple[py.path.local, List[str]]: """ return (fspath, names) tuple after checking the file exists. """ strpath, *parts = str(arg).split("::") if self.config.option.pyargs: @@ -628,7 +665,9 @@ def _parsearg(self, arg): fspath = fspath.realpath() return (fspath, parts) - def matchnodes(self, matching, names): + def matchnodes( + self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str], + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: self.trace("matchnodes", matching, names) self.trace.root.indent += 1 nodes = self._matchnodes(matching, names) @@ -639,13 +678,15 @@ def matchnodes(self, matching, names): raise NoMatch(matching, names[:1]) return nodes - def _matchnodes(self, matching, names): + def _matchnodes( + self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str], + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: if not matching or not names: return matching name = names[0] assert name nextnames = names[1:] - resultnodes = [] + resultnodes = [] # type: List[Union[nodes.Item, nodes.Collector]] for node in matching: if isinstance(node, nodes.Item): if not names: @@ -676,7 +717,9 @@ def _matchnodes(self, matching, names): node.ihook.pytest_collectreport(report=rep) return resultnodes - def genitems(self, node): + def genitems( + self, node: Union[nodes.Item, nodes.Collector] + ) -> Iterator[nodes.Item]: self.trace("genitems", node) if isinstance(node, nodes.Item): node.ihook.pytest_itemcollected(item=node) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index c9b633579e8..010dce925e4 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -4,8 +4,10 @@ from typing import Any from typing import Callable from typing import Dict +from typing import Iterable from typing import List from typing import Optional +from typing import Sequence from typing import Set from typing import Tuple from typing import Union @@ -226,7 +228,7 @@ def warn(self, warning): # methods for ordering nodes @property - def nodeid(self): + def nodeid(self) -> str: """ a ::-separated string denoting its collection tree address. """ return self._nodeid @@ -423,7 +425,7 @@ class Collector(Node): class CollectError(Exception): """ an error during collection, contains a custom message. """ - def collect(self): + def collect(self) -> Iterable[Union["Item", "Collector"]]: """ returns a list of children (items and collectors) for this collection node. """ @@ -522,6 +524,9 @@ def _gethookproxy(self, fspath: py.path.local): proxy = self.config.hook return proxy + def gethookproxy(self, fspath: py.path.local): + raise NotImplementedError() + def _recurse(self, dirpath: py.path.local) -> bool: if dirpath.basename == "__pycache__": return False @@ -535,7 +540,12 @@ def _recurse(self, dirpath: py.path.local) -> bool: ihook.pytest_collect_directory(path=dirpath, parent=self) return True - def _collectfile(self, path, handle_dupes=True): + def isinitpath(self, path: py.path.local) -> bool: + raise NotImplementedError() + + def _collectfile( + self, path: py.path.local, handle_dupes: bool = True + ) -> Sequence[Collector]: assert ( path.isfile() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( @@ -555,7 +565,7 @@ def _collectfile(self, path, handle_dupes=True): else: duplicate_paths.add(path) - return ihook.pytest_collect_file(path=path, parent=self) + return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] # noqa: F723 class File(FSCollector): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e46d498abd6..e05aa398dcb 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -43,9 +43,9 @@ from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass from _pytest.compat import STRING_TYPES +from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import ExitCode -from _pytest.compat import TYPE_CHECKING from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.deprecated import FUNCARGNAMES @@ -184,16 +184,20 @@ def pytest_pyfunc_call(pyfuncitem: "Function"): return True -def pytest_collect_file(path, parent): +def pytest_collect_file(path: py.path.local, parent) -> Optional["Module"]: ext = path.ext if ext == ".py": if not parent.session.isinitpath(path): if not path_matches_patterns( path, parent.config.getini("python_files") + ["__init__.py"] ): - return + return None ihook = parent.session.gethookproxy(path) - return ihook.pytest_pycollect_makemodule(path=path, parent=parent) + module = ihook.pytest_pycollect_makemodule( + path=path, parent=parent + ) # type: Module + return module + return None def path_matches_patterns(path, patterns): @@ -201,14 +205,16 @@ def path_matches_patterns(path, patterns): return any(path.fnmatch(pattern) for pattern in patterns) -def pytest_pycollect_makemodule(path, parent): +def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module": if path.basename == "__init__.py": - return Package.from_parent(parent, fspath=path) - return Module.from_parent(parent, fspath=path) + pkg = Package.from_parent(parent, fspath=path) # type: Package + return pkg + mod = Module.from_parent(parent, fspath=path) # type: Module + return mod @hookimpl(hookwrapper=True) -def pytest_pycollect_makeitem(collector, name, obj): +def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj): outcome = yield res = outcome.get_result() if res is not None: @@ -372,7 +378,7 @@ def _matches_prefix_or_glob_option(self, option_name, name): return True return False - def collect(self): + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: if not getattr(self.obj, "__test__", True): return [] @@ -381,8 +387,8 @@ def collect(self): dicts = [getattr(self.obj, "__dict__", {})] for basecls in self.obj.__class__.__mro__: dicts.append(basecls.__dict__) - seen = set() - values = [] + seen = set() # type: Set[str] + values = [] # type: List[Union[nodes.Item, nodes.Collector]] for dic in dicts: # Note: seems like the dict can change during iteration - # be careful not to remove the list() without consideration. @@ -404,9 +410,16 @@ def sort_key(item): values.sort(key=sort_key) return values - def _makeitem(self, name, obj): + def _makeitem( + self, name: str, obj + ) -> Union[ + None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]] + ]: # assert self.ihook.fspath == self.fspath, self - return self.ihook.pytest_pycollect_makeitem(collector=self, name=name, obj=obj) + item = self.ihook.pytest_pycollect_makeitem( + collector=self, name=name, obj=obj + ) # type: Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]] + return item def _genfunctions(self, name, funcobj): module = self.getparent(Module).obj @@ -458,7 +471,7 @@ class Module(nodes.File, PyCollector): def _getobj(self): return self._importtestmodule() - def collect(self): + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: self._inject_setup_module_fixture() self._inject_setup_function_fixture() self.session._fixturemanager.parsefactories(self) @@ -603,17 +616,17 @@ def setup(self): def gethookproxy(self, fspath: py.path.local): return super()._gethookproxy(fspath) - def isinitpath(self, path): + def isinitpath(self, path: py.path.local) -> bool: return path in self.session._initialpaths - def collect(self): + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: this_path = self.fspath.dirpath() init_module = this_path.join("__init__.py") if init_module.check(file=1) and path_matches_patterns( init_module, self.config.getini("python_files") ): yield Module.from_parent(self, fspath=init_module) - pkg_prefixes = set() + pkg_prefixes = set() # type: Set[py.path.local] for path in this_path.visit(rec=self._recurse, bf=True, sort=True): # We will visit our own __init__.py file, in which case we skip it. is_file = path.isfile() @@ -670,10 +683,11 @@ def from_parent(cls, parent, *, name, obj=None): """ return super().from_parent(name=name, parent=parent) - def collect(self): + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: if not safe_getattr(self.obj, "__test__", True): return [] if hasinit(self.obj): + assert self.parent is not None self.warn( PytestCollectionWarning( "cannot collect test class %r because it has a " @@ -683,6 +697,7 @@ def collect(self): ) return [] elif hasnew(self.obj): + assert self.parent is not None self.warn( PytestCollectionWarning( "cannot collect test class %r because it has a " @@ -756,7 +771,7 @@ class Instance(PyCollector): def _getobj(self): return self.parent.obj() - def collect(self): + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: self.session._fixturemanager.parsefactories(self) return super().collect() diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 178df6004f2..908ba7d3b4e 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -21,7 +21,8 @@ from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter from _pytest.compat import TYPE_CHECKING -from _pytest.nodes import Node +from _pytest.nodes import Collector +from _pytest.nodes import Item from _pytest.outcomes import skip from _pytest.pathlib import Path @@ -316,7 +317,13 @@ class CollectReport(BaseReport): when = "collect" def __init__( - self, nodeid: str, outcome, longrepr, result: List[Node], sections=(), **extra + self, + nodeid: str, + outcome, + longrepr, + result: Optional[List[Union[Item, Collector]]], + sections=(), + **extra ) -> None: self.nodeid = nodeid self.outcome = outcome diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index c7f6d8811ab..a2b9ee20789 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -404,10 +404,10 @@ def prepare(self, colitem) -> None: raise e -def collect_one_node(collector): +def collect_one_node(collector: Collector) -> CollectReport: ihook = collector.ihook ihook.pytest_collectstart(collector=collector) - rep = ihook.pytest_make_collect_report(collector=collector) + rep = ihook.pytest_make_collect_report(collector=collector) # type: CollectReport call = rep.__dict__.pop("call", None) if call and check_interactive_exception(call, rep): ihook.pytest_exception_interact(node=collector, call=call, report=rep) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 0d9133f6023..b2e6ab89d7d 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -1,32 +1,43 @@ """ discovery and running of std-library "unittest" style tests. """ import sys import traceback +from typing import Iterable +from typing import Optional +from typing import Union import _pytest._code import pytest from _pytest.compat import getimfunc from _pytest.compat import is_async_function from _pytest.config import hookimpl +from _pytest.nodes import Collector +from _pytest.nodes import Item from _pytest.outcomes import exit from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.outcomes import xfail from _pytest.python import Class from _pytest.python import Function +from _pytest.python import PyCollector from _pytest.runner import CallInfo from _pytest.skipping import skipped_by_mark_key from _pytest.skipping import unexpectedsuccess_key -def pytest_pycollect_makeitem(collector, name, obj): +def pytest_pycollect_makeitem( + collector: PyCollector, name: str, obj +) -> Optional["UnitTestCase"]: # has unittest been imported and is obj a subclass of its TestCase? try: - if not issubclass(obj, sys.modules["unittest"].TestCase): - return + ut = sys.modules["unittest"] + # Type ignored because `ut` is an opaque module. + if not issubclass(obj, ut.TestCase): # type: ignore + return None except Exception: - return + return None # yes, so let's collect it - return UnitTestCase.from_parent(collector, name=name, obj=obj) + item = UnitTestCase.from_parent(collector, name=name, obj=obj) # type: UnitTestCase + return item class UnitTestCase(Class): @@ -34,7 +45,7 @@ class UnitTestCase(Class): # to declare that our children do not support funcargs nofuncargs = True - def collect(self): + def collect(self) -> Iterable[Union[Item, Collector]]: from unittest import TestLoader cls = self.obj @@ -61,8 +72,8 @@ def collect(self): runtest = getattr(self.obj, "runTest", None) if runtest is not None: ut = sys.modules.get("twisted.trial.unittest", None) - if ut is None or runtest != ut.TestCase.runTest: - # TODO: callobj consistency + # Type ignored because `ut` is an opaque module. + if ut is None or runtest != ut.TestCase.runTest: # type: ignore yield TestCaseFunction.from_parent(self, name="runTest") def _inject_setup_teardown_fixtures(self, cls): From ef347295418451e1f09bfb9af1a77aba10b3e71c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:15 +0300 Subject: [PATCH 302/823] Type annotate fixtures.py & related --- src/_pytest/fixtures.py | 256 +++++++++++++++++++++++-------------- src/_pytest/hookspec.py | 10 +- src/_pytest/python.py | 3 +- src/_pytest/setuponly.py | 22 ++-- src/_pytest/setupplan.py | 6 +- testing/python/metafunc.py | 2 +- 6 files changed, 193 insertions(+), 106 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 4583e70f27c..4cd9a20ef64 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -5,9 +5,19 @@ import warnings from collections import defaultdict from collections import deque +from types import TracebackType +from typing import Any +from typing import Callable +from typing import cast from typing import Dict +from typing import Iterable +from typing import Iterator from typing import List +from typing import Optional +from typing import Sequence +from typing import Set from typing import Tuple +from typing import Union import attr import py @@ -29,6 +39,8 @@ from _pytest.compat import order_preserving_dict from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING +from _pytest.config import _PluggyPlugin +from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.deprecated import FILLFUNCARGS from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS @@ -38,16 +50,31 @@ from _pytest.outcomes import TEST_OUTCOME if TYPE_CHECKING: + from typing import NoReturn from typing import Type + from typing_extensions import Literal from _pytest import nodes from _pytest.main import Session + from _pytest.python import Metafunc + + _Scope = Literal["session", "package", "module", "class", "function"] + + +_FixtureCachedResult = Tuple[ + # The result. + Optional[object], + # Cache key. + object, + # Exc info if raised. + Optional[Tuple["Type[BaseException]", BaseException, TracebackType]], +] @attr.s(frozen=True) class PseudoFixtureDef: - cached_result = attr.ib() - scope = attr.ib() + cached_result = attr.ib(type="_FixtureCachedResult") + scope = attr.ib(type="_Scope") def pytest_sessionstart(session: "Session") -> None: @@ -92,7 +119,7 @@ def provide(self): return decoratescope -def get_scope_package(node, fixturedef): +def get_scope_package(node, fixturedef: "FixtureDef"): import pytest cls = pytest.Package @@ -114,7 +141,9 @@ def get_scope_node(node, scope): return node.getparent(cls) -def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager): +def add_funcarg_pseudo_fixture_def( + collector, metafunc: "Metafunc", fixturemanager: "FixtureManager" +) -> None: # this function will transform all collected calls to a functions # if they use direct funcargs (i.e. direct parametrization) # because we want later test execution to be able to rely on @@ -124,8 +153,8 @@ def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager): if not metafunc._calls[0].funcargs: return # this function call does not have direct parametrization # collect funcargs of all callspecs into a list of values - arg2params = {} - arg2scope = {} + arg2params = {} # type: Dict[str, List[object]] + arg2scope = {} # type: Dict[str, _Scope] for callspec in metafunc._calls: for argname, argvalue in callspec.funcargs.items(): assert argname not in callspec.params @@ -233,7 +262,7 @@ def reorder_items(items): return list(reorder_items_atscope(items, argkeys_cache, items_by_argkey, 0)) -def fix_cache_order(item, argkeys_cache, items_by_argkey): +def fix_cache_order(item, argkeys_cache, items_by_argkey) -> None: for scopenum in range(0, scopenum_function): for key in argkeys_cache[scopenum].get(item, []): items_by_argkey[scopenum][key].appendleft(item) @@ -279,7 +308,7 @@ def reorder_items_atscope(items, argkeys_cache, items_by_argkey, scopenum): return items_done -def fillfixtures(function): +def fillfixtures(function) -> None: """ fill missing funcargs for a test function. """ warnings.warn(FILLFUNCARGS, stacklevel=2) try: @@ -309,15 +338,15 @@ def get_direct_param_fixture_func(request): @attr.s(slots=True) class FuncFixtureInfo: # original function argument names - argnames = attr.ib(type=tuple) + argnames = attr.ib(type=Tuple[str, ...]) # argnames that function immediately requires. These include argnames + # fixture names specified via usefixtures and via autouse=True in fixture # definitions. - initialnames = attr.ib(type=tuple) - names_closure = attr.ib() # List[str] - name2fixturedefs = attr.ib() # List[str, List[FixtureDef]] + initialnames = attr.ib(type=Tuple[str, ...]) + names_closure = attr.ib(type=List[str]) + name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef"]]) - def prune_dependency_tree(self): + def prune_dependency_tree(self) -> None: """Recompute names_closure from initialnames and name2fixturedefs Can only reduce names_closure, which means that the new closure will @@ -328,7 +357,7 @@ def prune_dependency_tree(self): tree. In this way the dependency tree can get pruned, and the closure of argnames may get reduced. """ - closure = set() + closure = set() # type: Set[str] working_set = set(self.initialnames) while working_set: argname = working_set.pop() @@ -353,27 +382,29 @@ class FixtureRequest: the fixture is parametrized indirectly. """ - def __init__(self, pyfuncitem): + def __init__(self, pyfuncitem) -> None: self._pyfuncitem = pyfuncitem #: fixture for which this request is being performed - self.fixturename = None + self.fixturename = None # type: Optional[str] #: Scope string, one of "function", "class", "module", "session" - self.scope = "function" + self.scope = "function" # type: _Scope self._fixture_defs = {} # type: Dict[str, FixtureDef] - fixtureinfo = pyfuncitem._fixtureinfo + fixtureinfo = pyfuncitem._fixtureinfo # type: FuncFixtureInfo self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() - self._arg2index = {} - self._fixturemanager = pyfuncitem.session._fixturemanager + self._arg2index = {} # type: Dict[str, int] + self._fixturemanager = ( + pyfuncitem.session._fixturemanager + ) # type: FixtureManager @property - def fixturenames(self): + def fixturenames(self) -> List[str]: """names of all active fixtures in this request""" result = list(self._pyfuncitem._fixtureinfo.names_closure) result.extend(set(self._fixture_defs).difference(result)) return result @property - def funcargnames(self): + def funcargnames(self) -> List[str]: """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" warnings.warn(FUNCARGNAMES, stacklevel=2) return self.fixturenames @@ -383,15 +414,18 @@ def node(self): """ underlying collection node (depends on current request scope)""" return self._getscopeitem(self.scope) - def _getnextfixturedef(self, argname): + def _getnextfixturedef(self, argname: str) -> "FixtureDef": fixturedefs = self._arg2fixturedefs.get(argname, None) if fixturedefs is None: # we arrive here because of a dynamic call to # getfixturevalue(argname) usage which was naturally # not known at parsing/collection time + assert self._pyfuncitem.parent is not None parentid = self._pyfuncitem.parent.nodeid fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) - self._arg2fixturedefs[argname] = fixturedefs + # TODO: Fix this type ignore. Either add assert or adjust types. + # Can this be None here? + self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment] # noqa: F821 # fixturedefs list is immutable so we maintain a decreasing index index = self._arg2index.get(argname, 0) - 1 if fixturedefs is None or (-index > len(fixturedefs)): @@ -447,20 +481,20 @@ def session(self): """ pytest session object. """ return self._pyfuncitem.session - def addfinalizer(self, finalizer): + def addfinalizer(self, finalizer: Callable[[], object]) -> None: """ add finalizer/teardown function to be called after the last test within the requesting test context finished execution. """ # XXX usually this method is shadowed by fixturedef specific ones self._addfinalizer(finalizer, scope=self.scope) - def _addfinalizer(self, finalizer, scope): + def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None: colitem = self._getscopeitem(scope) self._pyfuncitem.session._setupstate.addfinalizer( finalizer=finalizer, colitem=colitem ) - def applymarker(self, marker): + def applymarker(self, marker) -> None: """ Apply a marker to a single test function invocation. This method is useful if you don't want to have a keyword/marker on all function invocations. @@ -470,18 +504,18 @@ def applymarker(self, marker): """ self.node.add_marker(marker) - def raiseerror(self, msg): + def raiseerror(self, msg: Optional[str]) -> "NoReturn": """ raise a FixtureLookupError with the given message. """ raise self._fixturemanager.FixtureLookupError(None, self, msg) - def _fillfixtures(self): + def _fillfixtures(self) -> None: item = self._pyfuncitem fixturenames = getattr(item, "fixturenames", self.fixturenames) for argname in fixturenames: if argname not in item.funcargs: item.funcargs[argname] = self.getfixturevalue(argname) - def getfixturevalue(self, argname): + def getfixturevalue(self, argname: str) -> Any: """ Dynamically run a named fixture function. Declaring fixtures via function argument is recommended where possible. @@ -492,9 +526,13 @@ def getfixturevalue(self, argname): :raise pytest.FixtureLookupError: If the given fixture could not be found. """ - return self._get_active_fixturedef(argname).cached_result[0] + fixturedef = self._get_active_fixturedef(argname) + assert fixturedef.cached_result is not None + return fixturedef.cached_result[0] - def _get_active_fixturedef(self, argname): + def _get_active_fixturedef( + self, argname: str + ) -> Union["FixtureDef", PseudoFixtureDef]: try: return self._fixture_defs[argname] except KeyError: @@ -503,7 +541,7 @@ def _get_active_fixturedef(self, argname): except FixtureLookupError: if argname == "request": cached_result = (self, [0], None) - scope = "function" + scope = "function" # type: _Scope return PseudoFixtureDef(cached_result, scope) raise # remove indent to prevent the python3 exception @@ -512,15 +550,16 @@ def _get_active_fixturedef(self, argname): self._fixture_defs[argname] = fixturedef return fixturedef - def _get_fixturestack(self): + def _get_fixturestack(self) -> List["FixtureDef"]: current = self - values = [] + values = [] # type: List[FixtureDef] while 1: fixturedef = getattr(current, "_fixturedef", None) if fixturedef is None: values.reverse() return values values.append(fixturedef) + assert isinstance(current, SubRequest) current = current._parent_request def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: @@ -593,13 +632,15 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: finally: self._schedule_finalizers(fixturedef, subrequest) - def _schedule_finalizers(self, fixturedef, subrequest): + def _schedule_finalizers( + self, fixturedef: "FixtureDef", subrequest: "SubRequest" + ) -> None: # if fixture function failed it might have registered finalizers self.session._setupstate.addfinalizer( functools.partial(fixturedef.finish, request=subrequest), subrequest.node ) - def _check_scope(self, argname, invoking_scope, requested_scope): + def _check_scope(self, argname, invoking_scope: "_Scope", requested_scope) -> None: if argname == "request": return if scopemismatch(invoking_scope, requested_scope): @@ -613,7 +654,7 @@ def _check_scope(self, argname, invoking_scope, requested_scope): pytrace=False, ) - def _factorytraceback(self): + def _factorytraceback(self) -> List[str]: lines = [] for fixturedef in self._get_fixturestack(): factory = fixturedef.func @@ -639,7 +680,7 @@ def _getscopeitem(self, scope): ) return node - def __repr__(self): + def __repr__(self) -> str: return "" % (self.node) @@ -647,9 +688,16 @@ class SubRequest(FixtureRequest): """ a sub request for handling getting a fixture from a test function/fixture. """ - def __init__(self, request, scope, param, param_index, fixturedef): + def __init__( + self, + request: "FixtureRequest", + scope: "_Scope", + param, + param_index: int, + fixturedef: "FixtureDef", + ) -> None: self._parent_request = request - self.fixturename = fixturedef.argname + self.fixturename = fixturedef.argname # type: str if param is not NOTSET: self.param = param self.param_index = param_index @@ -661,13 +709,15 @@ def __init__(self, request, scope, param, param_index, fixturedef): self._arg2index = request._arg2index self._fixturemanager = request._fixturemanager - def __repr__(self): + def __repr__(self) -> str: return "".format(self.fixturename, self._pyfuncitem) - def addfinalizer(self, finalizer): + def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._fixturedef.addfinalizer(finalizer) - def _schedule_finalizers(self, fixturedef, subrequest): + def _schedule_finalizers( + self, fixturedef: "FixtureDef", subrequest: "SubRequest" + ) -> None: # if the executing fixturedef was not explicitly requested in the argument list (via # getfixturevalue inside the fixture call) then ensure this fixture def will be finished # first @@ -678,20 +728,21 @@ def _schedule_finalizers(self, fixturedef, subrequest): super()._schedule_finalizers(fixturedef, subrequest) -scopes = "session package module class function".split() +scopes = ["session", "package", "module", "class", "function"] # type: List[_Scope] scopenum_function = scopes.index("function") -def scopemismatch(currentscope, newscope): +def scopemismatch(currentscope: "_Scope", newscope: "_Scope") -> bool: return scopes.index(newscope) > scopes.index(currentscope) -def scope2index(scope, descr, where=None): +def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int: """Look up the index of ``scope`` and raise a descriptive value error if not defined. """ + strscopes = scopes # type: Sequence[str] try: - return scopes.index(scope) + return strscopes.index(scope) except ValueError: fail( "{} {}got an unexpected scope value '{}'".format( @@ -704,7 +755,7 @@ def scope2index(scope, descr, where=None): class FixtureLookupError(LookupError): """ could not return a requested Fixture (missing or invalid). """ - def __init__(self, argname, request, msg=None): + def __init__(self, argname, request, msg: Optional[str] = None) -> None: self.argname = argname self.request = request self.fixturestack = request._get_fixturestack() @@ -782,14 +833,14 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line("%s:%d" % (self.filename, self.firstlineno + 1)) -def fail_fixturefunc(fixturefunc, msg): +def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn": fs, lineno = getfslineno(fixturefunc) location = "{}:{}".format(fs, lineno + 1) source = _pytest._code.Source(fixturefunc) fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) -def call_fixture_func(fixturefunc, request, kwargs): +def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs) -> object: yieldctx = is_generator(fixturefunc) if yieldctx: generator = fixturefunc(**kwargs) @@ -806,7 +857,7 @@ def call_fixture_func(fixturefunc, request, kwargs): return fixture_result -def _teardown_yield_fixture(fixturefunc, it): +def _teardown_yield_fixture(fixturefunc, it) -> None: """Executes the teardown of a fixture function by advancing the iterator after the yield and ensure the iteration ends (if not it means there is more than one yield in the function)""" try: @@ -819,7 +870,7 @@ def _teardown_yield_fixture(fixturefunc, it): ) -def _eval_scope_callable(scope_callable, fixture_name, config): +def _eval_scope_callable(scope_callable, fixture_name: str, config: Config) -> str: try: result = scope_callable(fixture_name=fixture_name, config=config) except Exception: @@ -843,15 +894,15 @@ class FixtureDef: def __init__( self, - fixturemanager, + fixturemanager: "FixtureManager", baseid, - argname, + argname: str, func, - scope, - params, - unittest=False, + scope: str, + params: Optional[Sequence[object]], + unittest: bool = False, ids=None, - ): + ) -> None: self._fixturemanager = fixturemanager self.baseid = baseid or "" self.has_location = baseid is not None @@ -859,23 +910,28 @@ def __init__( self.argname = argname if callable(scope): scope = _eval_scope_callable(scope, argname, fixturemanager.config) - self.scope = scope self.scopenum = scope2index( scope or "function", descr="Fixture '{}'".format(func.__name__), where=baseid, ) - self.params = params - self.argnames = getfuncargnames(func, name=argname, is_method=unittest) + # The cast is verified by scope2index. + # (Some of the type annotations below are supposed to be inferred, + # but mypy 0.761 has some trouble without them.) + self.scope = cast("_Scope", scope) # type: _Scope + self.params = params # type: Optional[Sequence[object]] + self.argnames = getfuncargnames( + func, name=argname, is_method=unittest + ) # type: Tuple[str, ...] self.unittest = unittest self.ids = ids - self.cached_result = None - self._finalizers = [] + self.cached_result = None # type: Optional[_FixtureCachedResult] + self._finalizers = [] # type: List[Callable[[], object]] - def addfinalizer(self, finalizer): + def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._finalizers.append(finalizer) - def finish(self, request): + def finish(self, request: SubRequest) -> None: exc = None try: while self._finalizers: @@ -899,12 +955,14 @@ def finish(self, request): self.cached_result = None self._finalizers = [] - def execute(self, request): + def execute(self, request: SubRequest): # get required arguments and register our own finish() # with their finalization for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) if argname != "request": + # PseudoFixtureDef is only for "request". + assert isinstance(fixturedef, FixtureDef) fixturedef.addfinalizer(functools.partial(self.finish, request=request)) my_cache_key = self.cache_key(request) @@ -926,16 +984,16 @@ def execute(self, request): hook = self._fixturemanager.session.gethookproxy(request.node.fspath) return hook.pytest_fixture_setup(fixturedef=self, request=request) - def cache_key(self, request): + def cache_key(self, request: SubRequest) -> object: return request.param_index if not hasattr(request, "param") else request.param - def __repr__(self): + def __repr__(self) -> str: return "".format( self.argname, self.scope, self.baseid ) -def resolve_fixture_function(fixturedef, request): +def resolve_fixture_function(fixturedef: FixtureDef, request: FixtureRequest): """Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific instances and bound methods. """ @@ -961,7 +1019,7 @@ def resolve_fixture_function(fixturedef, request): return fixturefunc -def pytest_fixture_setup(fixturedef, request): +def pytest_fixture_setup(fixturedef: FixtureDef, request: SubRequest) -> object: """ Execution of fixture setup. """ kwargs = {} for argname in fixturedef.argnames: @@ -976,7 +1034,9 @@ def pytest_fixture_setup(fixturedef, request): try: result = call_fixture_func(fixturefunc, request, kwargs) except TEST_OUTCOME: - fixturedef.cached_result = (None, my_cache_key, sys.exc_info()) + exc_info = sys.exc_info() + assert exc_info[0] is not None + fixturedef.cached_result = (None, my_cache_key, exc_info) raise fixturedef.cached_result = (result, my_cache_key, None) return result @@ -1190,7 +1250,7 @@ def yield_fixture( @fixture(scope="session") -def pytestconfig(request): +def pytestconfig(request: FixtureRequest): """Session-scoped fixture that returns the :class:`_pytest.config.Config` object. Example:: @@ -1247,15 +1307,17 @@ class FixtureManager: FixtureLookupError = FixtureLookupError FixtureLookupErrorRepr = FixtureLookupErrorRepr - def __init__(self, session): + def __init__(self, session: "Session") -> None: self.session = session - self.config = session.config - self._arg2fixturedefs = {} - self._holderobjseen = set() - self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] + self.config = session.config # type: Config + self._arg2fixturedefs = {} # type: Dict[str, List[FixtureDef]] + self._holderobjseen = set() # type: Set + self._nodeid_and_autousenames = [ + ("", self.config.getini("usefixtures")) + ] # type: List[Tuple[str, List[str]]] session.config.pluginmanager.register(self, "funcmanage") - def _get_direct_parametrize_args(self, node): + def _get_direct_parametrize_args(self, node) -> List[str]: """This function returns all the direct parametrization arguments of a node, so we don't mistake them for fixtures @@ -1264,7 +1326,7 @@ def _get_direct_parametrize_args(self, node): This things are done later as well when dealing with parametrization so this could be improved """ - parametrize_argnames = [] + parametrize_argnames = [] # type: List[str] for marker in node.iter_markers(name="parametrize"): if not marker.kwargs.get("indirect", False): p_argnames, _ = ParameterSet._parse_parametrize_args( @@ -1274,7 +1336,7 @@ def _get_direct_parametrize_args(self, node): return parametrize_argnames - def getfixtureinfo(self, node, func, cls, funcargs=True): + def getfixtureinfo(self, node, func, cls, funcargs: bool = True) -> FuncFixtureInfo: if funcargs and not getattr(node, "nofuncargs", False): argnames = getfuncargnames(func, name=node.name, cls=cls) else: @@ -1290,10 +1352,10 @@ def getfixtureinfo(self, node, func, cls, funcargs=True): ) return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) - def pytest_plugin_registered(self, plugin): + def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: nodeid = None try: - p = py.path.local(plugin.__file__).realpath() + p = py.path.local(plugin.__file__).realpath() # type: ignore[attr-defined] # noqa: F821 except AttributeError: pass else: @@ -1309,9 +1371,9 @@ def pytest_plugin_registered(self, plugin): self.parsefactories(plugin, nodeid) - def _getautousenames(self, nodeid): + def _getautousenames(self, nodeid: str) -> List[str]: """ return a tuple of fixture names to be used. """ - autousenames = [] + autousenames = [] # type: List[str] for baseid, basenames in self._nodeid_and_autousenames: if nodeid.startswith(baseid): if baseid: @@ -1322,7 +1384,9 @@ def _getautousenames(self, nodeid): autousenames.extend(basenames) return autousenames - def getfixtureclosure(self, fixturenames, parentnode, ignore_args=()): + def getfixtureclosure( + self, fixturenames: Tuple[str, ...], parentnode, ignore_args: Sequence[str] = () + ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef]]]: # collect the closure of all fixtures , starting with the given # fixturenames as the initial set. As we have to visit all # factory definitions anyway, we also return an arg2fixturedefs @@ -1333,7 +1397,7 @@ def getfixtureclosure(self, fixturenames, parentnode, ignore_args=()): parentid = parentnode.nodeid fixturenames_closure = self._getautousenames(parentid) - def merge(otherlist): + def merge(otherlist: Iterable[str]) -> None: for arg in otherlist: if arg not in fixturenames_closure: fixturenames_closure.append(arg) @@ -1345,7 +1409,7 @@ def merge(otherlist): # need to return it as well, so save this. initialnames = tuple(fixturenames_closure) - arg2fixturedefs = {} + arg2fixturedefs = {} # type: Dict[str, Sequence[FixtureDef]] lastlen = -1 while lastlen != len(fixturenames_closure): lastlen = len(fixturenames_closure) @@ -1359,7 +1423,7 @@ def merge(otherlist): arg2fixturedefs[argname] = fixturedefs merge(fixturedefs[-1].argnames) - def sort_by_scope(arg_name): + def sort_by_scope(arg_name: str) -> int: try: fixturedefs = arg2fixturedefs[arg_name] except KeyError: @@ -1370,7 +1434,7 @@ def sort_by_scope(arg_name): fixturenames_closure.sort(key=sort_by_scope) return initialnames, fixturenames_closure, arg2fixturedefs - def pytest_generate_tests(self, metafunc): + def pytest_generate_tests(self, metafunc: "Metafunc") -> None: for argname in metafunc.fixturenames: faclist = metafunc._arg2fixturedefs.get(argname) if faclist: @@ -1404,7 +1468,9 @@ def pytest_collection_modifyitems(self, items): # separate parametrized setups items[:] = reorder_items(items) - def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): + def parsefactories( + self, node_or_obj, nodeid=NOTSET, unittest: bool = False + ) -> None: if nodeid is not NOTSET: holderobj = node_or_obj else: @@ -1460,7 +1526,9 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): if autousenames: self._nodeid_and_autousenames.append((nodeid or "", autousenames)) - def getfixturedefs(self, argname, nodeid): + def getfixturedefs( + self, argname: str, nodeid: str + ) -> Optional[Sequence[FixtureDef]]: """ Gets a list of fixtures which are applicable to the given node id. @@ -1474,7 +1542,9 @@ def getfixturedefs(self, argname, nodeid): return None return tuple(self._matchfactories(fixturedefs, nodeid)) - def _matchfactories(self, fixturedefs, nodeid): + def _matchfactories( + self, fixturedefs: Iterable[FixtureDef], nodeid: str + ) -> Iterator[FixtureDef]: from _pytest import nodes for fixturedef in fixturedefs: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 1321eff540d..3f68860098b 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -20,6 +20,8 @@ from _pytest.config import PytestPluginManager from _pytest.config import _PluggyPlugin from _pytest.config.argparsing import Parser + from _pytest.fixtures import FixtureDef + from _pytest.fixtures import SubRequest from _pytest.main import Session from _pytest.nodes import Collector from _pytest.nodes import Item @@ -450,7 +452,9 @@ def pytest_report_from_serializable(config: "Config", data): @hookspec(firstresult=True) -def pytest_fixture_setup(fixturedef, request): +def pytest_fixture_setup( + fixturedef: "FixtureDef", request: "SubRequest" +) -> Optional[object]: """ performs fixture setup execution. :return: The return value of the call to the fixture function @@ -464,7 +468,9 @@ def pytest_fixture_setup(fixturedef, request): """ -def pytest_fixture_post_finalizer(fixturedef, request): +def pytest_fixture_post_finalizer( + fixturedef: "FixtureDef", request: "SubRequest" +) -> None: """Called after fixture teardown, but before the cache is cleared, so the fixture result ``fixturedef.cached_result`` is still available (not ``None``).""" diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e05aa398dcb..18b4fe2ff45 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -65,6 +65,7 @@ if TYPE_CHECKING: from typing_extensions import Literal + from _pytest.fixtures import _Scope def pytest_addoption(parser: Parser) -> None: @@ -905,7 +906,7 @@ def parametrize( Callable[[object], Optional[object]], ] ] = None, - scope: "Optional[str]" = None, + scope: "Optional[_Scope]" = None, *, _param_mark: Optional[Mark] = None ) -> None: diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index fe328d519c6..932d0c279b7 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -1,3 +1,4 @@ +from typing import Generator from typing import Optional from typing import Union @@ -6,6 +7,8 @@ from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import SubRequest def pytest_addoption(parser: Parser) -> None: @@ -25,7 +28,9 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(hookwrapper=True) -def pytest_fixture_setup(fixturedef, request): +def pytest_fixture_setup( + fixturedef: FixtureDef, request: SubRequest +) -> Generator[None, None, None]: yield if request.config.option.setupshow: if hasattr(request, "param"): @@ -33,24 +38,25 @@ def pytest_fixture_setup(fixturedef, request): # display it now and during the teardown (in .finish()). if fixturedef.ids: if callable(fixturedef.ids): - fixturedef.cached_param = fixturedef.ids(request.param) + param = fixturedef.ids(request.param) else: - fixturedef.cached_param = fixturedef.ids[request.param_index] + param = fixturedef.ids[request.param_index] else: - fixturedef.cached_param = request.param + param = request.param + fixturedef.cached_param = param # type: ignore[attr-defined] # noqa: F821 _show_fixture_action(fixturedef, "SETUP") -def pytest_fixture_post_finalizer(fixturedef) -> None: +def pytest_fixture_post_finalizer(fixturedef: FixtureDef) -> None: if fixturedef.cached_result is not None: config = fixturedef._fixturemanager.config if config.option.setupshow: _show_fixture_action(fixturedef, "TEARDOWN") if hasattr(fixturedef, "cached_param"): - del fixturedef.cached_param + del fixturedef.cached_param # type: ignore[attr-defined] # noqa: F821 -def _show_fixture_action(fixturedef, msg): +def _show_fixture_action(fixturedef: FixtureDef, msg: str) -> None: config = fixturedef._fixturemanager.config capman = config.pluginmanager.getplugin("capturemanager") if capman: @@ -73,7 +79,7 @@ def _show_fixture_action(fixturedef, msg): tw.write(" (fixtures used: {})".format(", ".join(deps))) if hasattr(fixturedef, "cached_param"): - tw.write("[{}]".format(saferepr(fixturedef.cached_param, maxsize=42))) + tw.write("[{}]".format(saferepr(fixturedef.cached_param, maxsize=42))) # type: ignore[attr-defined] tw.flush() diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 834d4ae2d51..0994ebbf207 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -5,6 +5,8 @@ from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import SubRequest def pytest_addoption(parser: Parser) -> None: @@ -19,7 +21,9 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(tryfirst=True) -def pytest_fixture_setup(fixturedef, request): +def pytest_fixture_setup( + fixturedef: FixtureDef, request: SubRequest +) -> Optional[object]: # Will return a dummy fixture if the setuponly option is provided. if request.config.option.setupplan: my_cache_key = fixturedef.cache_key(request) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 4d41910982b..c4b5bd22295 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -113,7 +113,7 @@ def func(x): fail.Exception, match=r"parametrize\(\) call in func got an unexpected scope value 'doggy'", ): - metafunc.parametrize("x", [1], scope="doggy") + metafunc.parametrize("x", [1], scope="doggy") # type: ignore[arg-type] # noqa: F821 def test_parametrize_request_name(self, testdir: Testdir) -> None: """Show proper error when 'request' is used as a parameter name in parametrize (#6183)""" From 247c4c0482888b18203589a2d0461d598bd2d817 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:15 +0300 Subject: [PATCH 303/823] Type annotate some more hooks & impls --- src/_pytest/assertion/__init__.py | 9 +++--- src/_pytest/assertion/rewrite.py | 6 ++-- src/_pytest/cacheprovider.py | 3 +- src/_pytest/capture.py | 11 ++++--- src/_pytest/debugging.py | 14 +++++++-- src/_pytest/faulthandler.py | 6 ++-- src/_pytest/hookspec.py | 46 +++++++++++++++++++---------- src/_pytest/junitxml.py | 6 ++-- src/_pytest/logging.py | 14 +++++---- src/_pytest/main.py | 3 +- src/_pytest/nose.py | 3 +- src/_pytest/pastebin.py | 24 +++++++-------- src/_pytest/pytester.py | 3 +- src/_pytest/python.py | 2 +- src/_pytest/reports.py | 11 +++++-- src/_pytest/resultlog.py | 4 ++- src/_pytest/runner.py | 49 ++++++++++++++++++------------- src/_pytest/skipping.py | 14 +++++---- src/_pytest/stepwise.py | 3 +- src/_pytest/terminal.py | 15 +++++++--- src/_pytest/unittest.py | 16 +++++++--- src/_pytest/warnings.py | 8 +++-- testing/test_runner.py | 4 +-- 23 files changed, 175 insertions(+), 99 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index e739816777a..997c1792192 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -4,6 +4,7 @@ import sys from typing import Any from typing import List +from typing import Generator from typing import Optional from _pytest.assertion import rewrite @@ -14,6 +15,7 @@ from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser +from _pytest.nodes import Item if TYPE_CHECKING: from _pytest.main import Session @@ -113,7 +115,7 @@ def pytest_collection(session: "Session") -> None: @hookimpl(tryfirst=True, hookwrapper=True) -def pytest_runtest_protocol(item): +def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks The rewrite module will use util._reprcompare if @@ -122,8 +124,7 @@ def pytest_runtest_protocol(item): comparison for the test. """ - def callbinrepr(op, left, right): - # type: (str, object, object) -> Optional[str] + def callbinrepr(op, left: object, right: object) -> Optional[str]: """Call the pytest_assertrepr_compare hook and prepare the result This uses the first result from the hook and then ensures the @@ -156,7 +157,7 @@ def callbinrepr(op, left, right): if item.ihook.pytest_assertion_pass.get_hookimpls(): - def call_assertion_pass_hook(lineno, orig, expl): + def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None: item.ihook.pytest_assertion_pass( item=item, lineno=lineno, orig=orig, expl=expl ) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ecec2aa3d23..bd4ea022c8d 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -444,14 +444,12 @@ def _call_reprcompare(ops, results, expls, each_obj): return expl -def _call_assertion_pass(lineno, orig, expl): - # type: (int, str, str) -> None +def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None: if util._assertion_pass is not None: util._assertion_pass(lineno, orig, expl) -def _check_if_assertion_pass_impl(): - # type: () -> bool +def _check_if_assertion_pass_impl() -> bool: """Checks if any plugins implement the pytest_assertion_pass hook in order not to generate explanation unecessarily (might be expensive)""" return True if util._assertion_pass else False diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index cd43c6cacc6..c95f171527f 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -29,6 +29,7 @@ from _pytest.config.argparsing import Parser from _pytest.main import Session from _pytest.python import Module +from _pytest.reports import TestReport README_CONTENT = """\ # pytest cache directory # @@ -265,7 +266,7 @@ def pytest_report_collectionfinish(self): if self.active and self.config.getoption("verbose") >= 0: return "run-last-failure: %s" % self._report_status - def pytest_runtest_logreport(self, report): + def pytest_runtest_logreport(self, report: TestReport) -> None: if (report.when == "call" and report.passed) or report.skipped: self.lastfailed.pop(report.nodeid, None) elif report.failed: diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 5a0cfff368e..13931ca103c 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -17,6 +17,9 @@ from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config.argparsing import Parser +from _pytest.fixtures import SubRequest +from _pytest.nodes import Collector +from _pytest.nodes import Item if TYPE_CHECKING: from typing_extensions import Literal @@ -710,7 +713,7 @@ def item_capture(self, when, item): # Hooks @pytest.hookimpl(hookwrapper=True) - def pytest_make_collect_report(self, collector): + def pytest_make_collect_report(self, collector: Collector): if isinstance(collector, pytest.File): self.resume_global_capture() outcome = yield @@ -725,17 +728,17 @@ def pytest_make_collect_report(self, collector): yield @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_setup(self, item): + def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: with self.item_capture("setup", item): yield @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_call(self, item): + def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]: with self.item_capture("call", item): yield @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_teardown(self, item): + def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]: with self.item_capture("teardown", item): yield diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 0085d319719..423b20ce3e9 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -4,12 +4,18 @@ import sys from _pytest import outcomes +from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.config import hookimpl from _pytest.config import PytestPluginManager from _pytest.config.argparsing import Parser from _pytest.config.exceptions import UsageError +from _pytest.nodes import Node +from _pytest.reports import BaseReport + +if TYPE_CHECKING: + from _pytest.runner import CallInfo def _validate_usepdb_cls(value): @@ -259,7 +265,9 @@ def set_trace(cls, *args, **kwargs): class PdbInvoke: - def pytest_exception_interact(self, node, call, report): + def pytest_exception_interact( + self, node: Node, call: "CallInfo", report: BaseReport + ) -> None: capman = node.config.pluginmanager.getplugin("capturemanager") if capman: capman.suspend_global_capture(in_=True) @@ -306,7 +314,7 @@ def maybe_wrap_pytest_function_for_tracing(pyfuncitem): wrap_pytest_function_for_tracing(pyfuncitem) -def _enter_pdb(node, excinfo, rep): +def _enter_pdb(node: Node, excinfo, rep: BaseReport) -> BaseReport: # XXX we re-use the TerminalReporter's terminalwriter # because this seems to avoid some encoding related troubles # for not completely clear reasons. @@ -330,7 +338,7 @@ def _enter_pdb(node, excinfo, rep): rep.toterminal(tw) tw.sep(">", "entering PDB") tb = _postmortem_traceback(excinfo) - rep._pdbshown = True + rep._pdbshown = True # type: ignore[attr-defined] # noqa: F821 post_mortem(tb) return rep diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 9d777b415ea..79936b78f9a 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -1,11 +1,13 @@ import io import os import sys +from typing import Generator from typing import TextIO import pytest from _pytest.config import Config from _pytest.config.argparsing import Parser +from _pytest.nodes import Item from _pytest.store import StoreKey @@ -82,7 +84,7 @@ def get_timeout_config_value(config): return float(config.getini("faulthandler_timeout") or 0.0) @pytest.hookimpl(hookwrapper=True, trylast=True) - def pytest_runtest_protocol(self, item): + def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: timeout = self.get_timeout_config_value(item.config) stderr = item.config._store[fault_handler_stderr_key] if timeout > 0 and stderr is not None: @@ -105,7 +107,7 @@ def pytest_enter_pdb(self): faulthandler.cancel_dump_traceback_later() @pytest.hookimpl(tryfirst=True) - def pytest_exception_interact(self): + def pytest_exception_interact(self) -> None: """Cancel any traceback dumping due to an interactive exception being raised. """ diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 3f68860098b..ccdb0bde930 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -25,10 +25,16 @@ from _pytest.main import Session from _pytest.nodes import Collector from _pytest.nodes import Item + from _pytest.nodes import Node + from _pytest.python import Function from _pytest.python import Metafunc from _pytest.python import Module from _pytest.python import PyCollector from _pytest.reports import BaseReport + from _pytest.reports import CollectReport + from _pytest.reports import TestReport + from _pytest.runner import CallInfo + from _pytest.terminal import TerminalReporter hookspec = HookspecMarker("pytest") @@ -268,7 +274,7 @@ def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]": # logging hooks for collection -def pytest_collectstart(collector): +def pytest_collectstart(collector: "Collector") -> None: """ collector starts collecting. """ @@ -285,7 +291,7 @@ def pytest_deselected(items): @hookspec(firstresult=True) -def pytest_make_collect_report(collector): +def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectReport]": """ perform ``collector.collect()`` and return a CollectReport. Stops at first non-None result, see :ref:`firstresult` """ @@ -319,7 +325,7 @@ def pytest_pycollect_makeitem( @hookspec(firstresult=True) -def pytest_pyfunc_call(pyfuncitem): +def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: """ call underlying test function. Stops at first non-None result, see :ref:`firstresult` """ @@ -330,7 +336,9 @@ def pytest_generate_tests(metafunc: "Metafunc") -> None: @hookspec(firstresult=True) -def pytest_make_parametrize_id(config: "Config", val, argname) -> Optional[str]: +def pytest_make_parametrize_id( + config: "Config", val: object, argname: str +) -> Optional[str]: """Return a user-friendly string representation of the given ``val`` that will be used by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``. The parameter name is available as ``argname``, if required. @@ -349,7 +357,7 @@ def pytest_make_parametrize_id(config: "Config", val, argname) -> Optional[str]: @hookspec(firstresult=True) -def pytest_runtestloop(session: "Session"): +def pytest_runtestloop(session: "Session") -> Optional[object]: """ called for performing the main runtest loop (after collection finished). @@ -360,7 +368,9 @@ def pytest_runtestloop(session: "Session"): @hookspec(firstresult=True) -def pytest_runtest_protocol(item, nextitem): +def pytest_runtest_protocol( + item: "Item", nextitem: "Optional[Item]" +) -> Optional[object]: """ implements the runtest_setup/call/teardown protocol for the given test item, including capturing exceptions and calling reporting hooks. @@ -399,15 +409,15 @@ def pytest_runtest_logfinish(nodeid, location): """ -def pytest_runtest_setup(item): +def pytest_runtest_setup(item: "Item") -> None: """ called before ``pytest_runtest_call(item)``. """ -def pytest_runtest_call(item): +def pytest_runtest_call(item: "Item") -> None: """ called to execute the test ``item``. """ -def pytest_runtest_teardown(item, nextitem): +def pytest_runtest_teardown(item: "Item", nextitem: "Optional[Item]") -> None: """ called after ``pytest_runtest_call``. :arg nextitem: the scheduled-to-be-next test item (None if no further @@ -418,7 +428,7 @@ def pytest_runtest_teardown(item, nextitem): @hookspec(firstresult=True) -def pytest_runtest_makereport(item, call): +def pytest_runtest_makereport(item: "Item", call: "CallInfo") -> Optional[object]: """ return a :py:class:`_pytest.runner.TestReport` object for the given :py:class:`pytest.Item <_pytest.main.Item>` and :py:class:`_pytest.runner.CallInfo`. @@ -426,7 +436,7 @@ def pytest_runtest_makereport(item, call): Stops at first non-None result, see :ref:`firstresult` """ -def pytest_runtest_logreport(report): +def pytest_runtest_logreport(report: "TestReport") -> None: """ process a test setup/call/teardown report relating to the respective phase of executing a test. """ @@ -511,7 +521,9 @@ def pytest_unconfigure(config: "Config") -> None: # ------------------------------------------------------------------------- -def pytest_assertrepr_compare(config: "Config", op, left, right): +def pytest_assertrepr_compare( + config: "Config", op: str, left: object, right: object +) -> Optional[List[str]]: """return explanation for comparisons in failing assert expressions. Return None for no custom explanation, otherwise return a list @@ -523,7 +535,7 @@ def pytest_assertrepr_compare(config: "Config", op, left, right): """ -def pytest_assertion_pass(item, lineno, orig, expl): +def pytest_assertion_pass(item, lineno: int, orig: str, expl: str) -> None: """ **(Experimental)** @@ -637,7 +649,9 @@ def pytest_report_teststatus( """ -def pytest_terminal_summary(terminalreporter, exitstatus, config: "Config"): +def pytest_terminal_summary( + terminalreporter: "TerminalReporter", exitstatus: "ExitCode", config: "Config", +) -> None: """Add a section to terminal summary reporting. :param _pytest.terminal.TerminalReporter terminalreporter: the internal terminal reporter object @@ -741,7 +755,9 @@ def pytest_keyboard_interrupt(excinfo): """ called for keyboard interrupt. """ -def pytest_exception_interact(node, call, report): +def pytest_exception_interact( + node: "Node", call: "CallInfo", report: "BaseReport" +) -> None: """called when an exception was raised which can potentially be interactively handled. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index b0790bc794d..0ecfb09bb54 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -24,7 +24,9 @@ from _pytest.config import Config from _pytest.config import filename_arg from _pytest.config.argparsing import Parser +from _pytest.reports import TestReport from _pytest.store import StoreKey +from _pytest.terminal import TerminalReporter from _pytest.warnings import _issue_warning_captured @@ -517,7 +519,7 @@ def _opentestcase(self, report): reporter.record_testreport(report) return reporter - def pytest_runtest_logreport(self, report): + def pytest_runtest_logreport(self, report: TestReport) -> None: """handle a setup/call/teardown report, generating the appropriate xml tags as necessary. @@ -661,7 +663,7 @@ def pytest_sessionfinish(self) -> None: logfile.write(Junit.testsuites([suite_node]).unicode(indent=0)) logfile.close() - def pytest_terminal_summary(self, terminalreporter): + def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None: terminalreporter.write_sep("-", "generated xml file: {}".format(self.logfile)) def add_global_property(self, name, value): diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index b832f6994fb..92046ed51d6 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -20,6 +20,7 @@ from _pytest.config import Config from _pytest.config import create_terminal_writer from _pytest.config.argparsing import Parser +from _pytest.main import Session from _pytest.pathlib import Path from _pytest.store import StoreKey @@ -618,7 +619,7 @@ def pytest_collection(self) -> Generator[None, None, None]: yield @pytest.hookimpl(hookwrapper=True) - def pytest_runtestloop(self, session): + def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]: """Runs all collected test items.""" if session.config.option.collectonly: @@ -655,20 +656,21 @@ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, Non item.add_report_section(when, "log", log) @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_setup(self, item): + def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("setup") - item._store[catch_log_records_key] = {} + empty = {} # type: Dict[str, List[logging.LogRecord]] + item._store[catch_log_records_key] = empty yield from self._runtest_for(item, "setup") @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_call(self, item): + 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") @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_teardown(self, item): + 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") @@ -676,7 +678,7 @@ def pytest_runtest_teardown(self, item): del item._store[catch_log_handler_key] @pytest.hookimpl - def pytest_runtest_logfinish(self): + def pytest_runtest_logfinish(self) -> None: self.log_cli_handler.set_when("finish") @pytest.hookimpl(hookwrapper=True, tryfirst=True) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index a0007d226df..d891335a749 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -31,6 +31,7 @@ from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit from _pytest.reports import CollectReport +from _pytest.reports import TestReport from _pytest.runner import collect_one_node from _pytest.runner import SetupState @@ -441,7 +442,7 @@ def pytest_collectstart(self) -> None: raise self.Interrupted(self.shouldstop) @hookimpl(tryfirst=True) - def pytest_runtest_logreport(self, report) -> None: + def pytest_runtest_logreport(self, report: TestReport) -> None: if report.failed and not hasattr(report, "wasxfail"): self.testsfailed += 1 maxfail = self.config.getvalue("maxfail") diff --git a/src/_pytest/nose.py b/src/_pytest/nose.py index d6f3c2b224a..8bdc310ac18 100644 --- a/src/_pytest/nose.py +++ b/src/_pytest/nose.py @@ -2,6 +2,7 @@ from _pytest import python from _pytest import unittest from _pytest.config import hookimpl +from _pytest.nodes import Item @hookimpl(trylast=True) @@ -20,7 +21,7 @@ def teardown_nose(item): call_optional(item.parent.obj, "teardown") -def is_potential_nosetest(item): +def is_potential_nosetest(item: Item) -> bool: # extra check needed since we do not do nose style setup/teardown # on direct unittest style classes return isinstance(item, python.Function) and not isinstance( diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 091d3f81762..7e6bbf50cbe 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -2,11 +2,14 @@ import tempfile from io import StringIO from typing import IO +from typing import Union import pytest from _pytest.config import Config +from _pytest.config import create_terminal_writer from _pytest.config.argparsing import Parser from _pytest.store import StoreKey +from _pytest.terminal import TerminalReporter pastebinfile_key = StoreKey[IO[bytes]]() @@ -63,11 +66,11 @@ def pytest_unconfigure(config: Config) -> None: tr.write_line("pastebin session-log: %s\n" % pastebinurl) -def create_new_paste(contents): +def create_new_paste(contents: Union[str, bytes]) -> str: """ Creates a new paste using bpaste.net service. - :contents: paste contents as utf-8 encoded bytes + :contents: paste contents string :returns: url to the pasted contents or error message """ import re @@ -79,7 +82,7 @@ def create_new_paste(contents): try: response = ( urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8") - ) + ) # type: str except OSError as exc_info: # urllib errors return "bad response: %s" % exc_info m = re.search(r'href="/raw/(\w+)"', response) @@ -89,23 +92,20 @@ def create_new_paste(contents): return "bad response: invalid format ('" + response + "')" -def pytest_terminal_summary(terminalreporter): - import _pytest.config - +def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None: if terminalreporter.config.option.pastebin != "failed": return - tr = terminalreporter - if "failed" in tr.stats: + if "failed" in terminalreporter.stats: terminalreporter.write_sep("=", "Sending information to Paste Service") - for rep in terminalreporter.stats.get("failed"): + for rep in terminalreporter.stats["failed"]: try: msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc except AttributeError: - msg = tr._getfailureheadline(rep) + msg = terminalreporter._getfailureheadline(rep) file = StringIO() - tw = _pytest.config.create_terminal_writer(terminalreporter.config, file) + tw = create_terminal_writer(terminalreporter.config, file) rep.toterminal(tw) s = file.getvalue() assert len(s) pastebinurl = create_new_paste(s) - tr.write_line("{} --> {}".format(msg, pastebinurl)) + terminalreporter.write_line("{} --> {}".format(msg, pastebinurl)) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index ae7bdcec8e8..12ad0591c4a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -12,6 +12,7 @@ from io import StringIO from typing import Callable from typing import Dict +from typing import Generator from typing import Iterable from typing import List from typing import Optional @@ -138,7 +139,7 @@ def matching_platform(self): return True @pytest.hookimpl(hookwrapper=True, tryfirst=True) - def pytest_runtest_protocol(self, item): + def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: lines1 = self.get_open_files() yield if hasattr(sys, "pypy_version_info"): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 18b4fe2ff45..9b8dcf60868 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -173,7 +173,7 @@ def async_warn_and_skip(nodeid: str) -> None: @hookimpl(trylast=True) -def pytest_pyfunc_call(pyfuncitem: "Function"): +def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: testfunction = pyfuncitem.obj if is_async_function(testfunction): async_warn_and_skip(pyfuncitem.nodeid) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 908ba7d3b4e..9763cb4ade3 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -26,6 +26,9 @@ from _pytest.outcomes import skip from _pytest.pathlib import Path +if TYPE_CHECKING: + from _pytest.runner import CallInfo + def getslaveinfoline(node): try: @@ -42,7 +45,8 @@ def getslaveinfoline(node): class BaseReport: when = None # type: Optional[str] location = None # type: Optional[Tuple[str, Optional[int], str]] - longrepr = None + # TODO: Improve this Any. + longrepr = None # type: Optional[Any] sections = [] # type: List[Tuple[str, str]] nodeid = None # type: str @@ -270,7 +274,7 @@ def __repr__(self): ) @classmethod - def from_item_and_call(cls, item, call) -> "TestReport": + def from_item_and_call(cls, item: Item, call: "CallInfo") -> "TestReport": """ Factory method to create and fill a TestReport with standard item and call info. """ @@ -281,7 +285,8 @@ def from_item_and_call(cls, item, call) -> "TestReport": sections = [] if not call.excinfo: outcome = "passed" - longrepr = None + # TODO: Improve this Any. + longrepr = None # type: Optional[Any] else: if not isinstance(excinfo, ExceptionInfo): outcome = "failed" diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index acc89afe2b7..720ea9f490c 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -7,6 +7,7 @@ from _pytest.config import Config from _pytest.config.argparsing import Parser +from _pytest.reports import TestReport from _pytest.store import StoreKey @@ -66,7 +67,7 @@ def log_outcome(self, report, lettercode, longrepr): testpath = report.fspath self.write_log_entry(testpath, lettercode, longrepr) - def pytest_runtest_logreport(self, report): + def pytest_runtest_logreport(self, report: TestReport) -> None: if report.when != "call" and report.passed: return res = self.config.hook.pytest_report_teststatus( @@ -80,6 +81,7 @@ def pytest_runtest_logreport(self, report): elif report.passed: longrepr = "" elif report.skipped: + assert report.longrepr is not None longrepr = str(report.longrepr[2]) else: longrepr = str(report.longrepr) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index a2b9ee20789..568065d948e 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -10,6 +10,7 @@ import attr +from .reports import BaseReport from .reports import CollectErrorRepr from .reports import CollectReport from .reports import TestReport @@ -19,6 +20,7 @@ from _pytest.compat import TYPE_CHECKING from _pytest.config.argparsing import Parser from _pytest.nodes import Collector +from _pytest.nodes import Item from _pytest.nodes import Node from _pytest.outcomes import Exit from _pytest.outcomes import Skipped @@ -29,6 +31,7 @@ from typing_extensions import Literal from _pytest.main import Session + from _pytest.terminal import TerminalReporter # # pytest plugin hooks @@ -46,7 +49,7 @@ def pytest_addoption(parser: Parser) -> None: ) -def pytest_terminal_summary(terminalreporter): +def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: durations = terminalreporter.config.option.durations verbose = terminalreporter.config.getvalue("verbose") if durations is None: @@ -86,17 +89,19 @@ def pytest_sessionfinish(session: "Session") -> None: session._setupstate.teardown_all() -def pytest_runtest_protocol(item, nextitem): +def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool: item.ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) runtestprotocol(item, nextitem=nextitem) item.ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) return True -def runtestprotocol(item, log=True, nextitem=None): +def runtestprotocol( + item: Item, log: bool = True, nextitem: Optional[Item] = None +) -> List[TestReport]: hasrequest = hasattr(item, "_request") - if hasrequest and not item._request: - item._initrequest() + if hasrequest and not item._request: # type: ignore[attr-defined] # noqa: F821 + item._initrequest() # type: ignore[attr-defined] # noqa: F821 rep = call_and_report(item, "setup", log) reports = [rep] if rep.passed: @@ -108,12 +113,12 @@ def runtestprotocol(item, log=True, nextitem=None): # after all teardown hooks have been called # want funcargs and request info to go away if hasrequest: - item._request = False - item.funcargs = None + item._request = False # type: ignore[attr-defined] # noqa: F821 + item.funcargs = None # type: ignore[attr-defined] # noqa: F821 return reports -def show_test_item(item): +def show_test_item(item: Item) -> None: """Show test function, parameters and the fixtures of the test item.""" tw = item.config.get_terminal_writer() tw.line() @@ -125,12 +130,12 @@ def show_test_item(item): tw.flush() -def pytest_runtest_setup(item): +def pytest_runtest_setup(item: Item) -> None: _update_current_test_var(item, "setup") item.session._setupstate.prepare(item) -def pytest_runtest_call(item): +def pytest_runtest_call(item: Item) -> None: _update_current_test_var(item, "call") try: del sys.last_type @@ -150,13 +155,15 @@ def pytest_runtest_call(item): raise e -def pytest_runtest_teardown(item, nextitem): +def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None: _update_current_test_var(item, "teardown") item.session._setupstate.teardown_exact(item, nextitem) _update_current_test_var(item, None) -def _update_current_test_var(item, when): +def _update_current_test_var( + item: Item, when: Optional["Literal['setup', 'call', 'teardown']"] +) -> None: """ Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage. @@ -188,11 +195,11 @@ def pytest_report_teststatus(report): def call_and_report( - item, when: "Literal['setup', 'call', 'teardown']", log=True, **kwds -): + item: Item, when: "Literal['setup', 'call', 'teardown']", log: bool = True, **kwds +) -> TestReport: call = call_runtest_hook(item, when, **kwds) hook = item.ihook - report = hook.pytest_runtest_makereport(item=item, call=call) + report = hook.pytest_runtest_makereport(item=item, call=call) # type: TestReport if log: hook.pytest_runtest_logreport(report=report) if check_interactive_exception(call, report): @@ -200,15 +207,17 @@ def call_and_report( return report -def check_interactive_exception(call, report): - return call.excinfo and not ( +def check_interactive_exception(call: "CallInfo", report: BaseReport) -> bool: + return call.excinfo is not None and not ( hasattr(report, "wasxfail") or call.excinfo.errisinstance(Skipped) or call.excinfo.errisinstance(bdb.BdbQuit) ) -def call_runtest_hook(item, when: "Literal['setup', 'call', 'teardown']", **kwds): +def call_runtest_hook( + item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds +) -> "CallInfo": if when == "setup": ihook = item.ihook.pytest_runtest_setup elif when == "call": @@ -278,13 +287,13 @@ def from_call(cls, func, when, reraise=None) -> "CallInfo": excinfo=excinfo, ) - def __repr__(self): + def __repr__(self) -> str: if self.excinfo is None: return "".format(self.when, self._result) return "".format(self.when, self.excinfo) -def pytest_runtest_makereport(item, call): +def pytest_runtest_makereport(item: Item, call: CallInfo) -> TestReport: return TestReport.from_item_and_call(item, call) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 5e5fcc080e1..5994b5b2fa1 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -3,9 +3,12 @@ from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.mark.evaluate import MarkEvaluator +from _pytest.nodes import Item from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.outcomes import xfail +from _pytest.python import Function +from _pytest.runner import CallInfo from _pytest.store import StoreKey @@ -74,7 +77,7 @@ def nop(*args, **kwargs): @hookimpl(tryfirst=True) -def pytest_runtest_setup(item): +def pytest_runtest_setup(item: Item) -> None: # Check if skip or skipif are specified as pytest marks item._store[skipped_by_mark_key] = False eval_skipif = MarkEvaluator(item, "skipif") @@ -96,7 +99,7 @@ def pytest_runtest_setup(item): @hookimpl(hookwrapper=True) -def pytest_pyfunc_call(pyfuncitem): +def pytest_pyfunc_call(pyfuncitem: Function): check_xfail_no_run(pyfuncitem) outcome = yield passed = outcome.excinfo is None @@ -104,7 +107,7 @@ def pytest_pyfunc_call(pyfuncitem): check_strict_xfail(pyfuncitem) -def check_xfail_no_run(item): +def check_xfail_no_run(item: Item) -> None: """check xfail(run=False)""" if not item.config.option.runxfail: evalxfail = item._store[evalxfail_key] @@ -113,7 +116,7 @@ def check_xfail_no_run(item): xfail("[NOTRUN] " + evalxfail.getexplanation()) -def check_strict_xfail(pyfuncitem): +def check_strict_xfail(pyfuncitem: Function) -> None: """check xfail(strict=True) for the given PASSING test""" evalxfail = pyfuncitem._store[evalxfail_key] if evalxfail.istrue(): @@ -126,7 +129,7 @@ def check_strict_xfail(pyfuncitem): @hookimpl(hookwrapper=True) -def pytest_runtest_makereport(item, call): +def pytest_runtest_makereport(item: Item, call: CallInfo): outcome = yield rep = outcome.get_result() evalxfail = item._store.get(evalxfail_key, None) @@ -171,6 +174,7 @@ def pytest_runtest_makereport(item, call): # the location of where the skip exception was raised within pytest _, _, reason = rep.longrepr filename, line = item.reportinfo()[:2] + assert line is not None rep.longrepr = str(filename), line + 1, reason diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 3cbf0be9fc0..1921245dc18 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -2,6 +2,7 @@ from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.main import Session +from _pytest.reports import TestReport def pytest_addoption(parser: Parser) -> None: @@ -73,7 +74,7 @@ def pytest_collection_modifyitems(self, session, config, items): config.hook.pytest_deselected(items=already_passed) - def pytest_runtest_logreport(self, report): + def pytest_runtest_logreport(self, report: TestReport) -> None: if not self.active: return diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 6f4b96e1e16..bc2b5bf2323 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -12,6 +12,7 @@ from typing import Any from typing import Callable from typing import Dict +from typing import Generator from typing import List from typing import Mapping from typing import Optional @@ -30,15 +31,19 @@ from _pytest._io import TerminalWriter from _pytest._io.wcwidth import wcswidth from _pytest.compat import order_preserving_dict +from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser from _pytest.deprecated import TERMINALWRITER_WRITER -from _pytest.main import Session from _pytest.reports import CollectReport from _pytest.reports import TestReport +if TYPE_CHECKING: + from _pytest.main import Session + + REPORT_COLLECTING_RESOLUTION = 0.5 KNOWN_TYPES = ( @@ -610,7 +615,7 @@ def report_collect(self, final=False): self.write_line(line) @pytest.hookimpl(trylast=True) - def pytest_sessionstart(self, session: Session) -> None: + def pytest_sessionstart(self, session: "Session") -> None: self._session = session self._sessionstarttime = timing.time() if not self.showheader: @@ -720,7 +725,9 @@ def _printcollecteditems(self, items): self._tw.line("{}{}".format(indent + " ", line)) @pytest.hookimpl(hookwrapper=True) - def pytest_sessionfinish(self, session: Session, exitstatus: Union[int, ExitCode]): + def pytest_sessionfinish( + self, session: "Session", exitstatus: Union[int, ExitCode] + ): outcome = yield outcome.get_result() self._tw.line("") @@ -745,7 +752,7 @@ def pytest_sessionfinish(self, session: Session, exitstatus: Union[int, ExitCode self.summary_stats() @pytest.hookimpl(hookwrapper=True) - def pytest_terminal_summary(self): + def pytest_terminal_summary(self) -> Generator[None, None, None]: self.summary_errors() self.summary_failures() self.summary_warnings() diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index b2e6ab89d7d..3fbf7c88dbe 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -1,6 +1,8 @@ """ discovery and running of std-library "unittest" style tests. """ import sys import traceback +from typing import Any +from typing import Generator from typing import Iterable from typing import Optional from typing import Union @@ -253,7 +255,7 @@ def _prunetraceback(self, excinfo): @hookimpl(tryfirst=True) -def pytest_runtest_makereport(item, call): +def pytest_runtest_makereport(item: Item, call: CallInfo) -> None: if isinstance(item, TestCaseFunction): if item._excinfo: call.excinfo = item._excinfo.pop(0) @@ -263,7 +265,13 @@ def pytest_runtest_makereport(item, call): pass unittest = sys.modules.get("unittest") - if unittest and call.excinfo and call.excinfo.errisinstance(unittest.SkipTest): + if ( + unittest + and call.excinfo + and call.excinfo.errisinstance( + unittest.SkipTest # type: ignore[attr-defined] # noqa: F821 + ) + ): # let's substitute the excinfo with a pytest.skip one call2 = CallInfo.from_call( lambda: pytest.skip(str(call.excinfo.value)), call.when @@ -275,9 +283,9 @@ def pytest_runtest_makereport(item, call): @hookimpl(hookwrapper=True) -def pytest_runtest_protocol(item): +def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: - ut = sys.modules["twisted.python.failure"] + ut = sys.modules["twisted.python.failure"] # type: Any Failure__init__ = ut.Failure.__init__ check_testcase_implements_trial_reporter() diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 83e338cb3d4..622cbb806a2 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -11,6 +11,8 @@ from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.main import Session +from _pytest.nodes import Item +from _pytest.terminal import TerminalReporter if TYPE_CHECKING: from typing_extensions import Type @@ -145,7 +147,7 @@ def warning_record_to_str(warning_message): @pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_protocol(item): +def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: with catch_warnings_for_item( config=item.config, ihook=item.ihook, when="runtest", item=item ): @@ -162,7 +164,9 @@ def pytest_collection(session: Session) -> Generator[None, None, None]: @pytest.hookimpl(hookwrapper=True) -def pytest_terminal_summary(terminalreporter): +def pytest_terminal_summary( + terminalreporter: TerminalReporter, +) -> Generator[None, None, None]: config = terminalreporter.config with catch_warnings_for_item( config=config, ihook=config.hook, when="config", item=None diff --git a/testing/test_runner.py b/testing/test_runner.py index 32620801d53..be79b14fd10 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -884,7 +884,7 @@ def runtest(self): raise IndexError("TEST") try: - runner.pytest_runtest_call(ItemMightRaise()) + runner.pytest_runtest_call(ItemMightRaise()) # type: ignore[arg-type] # noqa: F821 except IndexError: pass # Check that exception info is stored on sys @@ -895,7 +895,7 @@ def runtest(self): # The next run should clear the exception info stored by the previous run ItemMightRaise.raise_error = False - runner.pytest_runtest_call(ItemMightRaise()) + runner.pytest_runtest_call(ItemMightRaise()) # type: ignore[arg-type] # noqa: F821 assert not hasattr(sys, "last_type") assert not hasattr(sys, "last_value") assert not hasattr(sys, "last_traceback") From 30e3d473c4addd5fc906ee3fb6da438e28daf7b8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:15 +0300 Subject: [PATCH 304/823] Type annotate _pytest._io.saferepr --- src/_pytest/_io/saferepr.py | 43 ++++++++++++++++++++++++------- src/_pytest/assertion/__init__.py | 2 +- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 47a00de6063..6b9f353a227 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -1,9 +1,12 @@ import pprint import reprlib from typing import Any +from typing import Dict +from typing import IO +from typing import Optional -def _try_repr_or_str(obj): +def _try_repr_or_str(obj: object) -> str: try: return repr(obj) except (KeyboardInterrupt, SystemExit): @@ -12,7 +15,7 @@ def _try_repr_or_str(obj): return '{}("{}")'.format(type(obj).__name__, obj) -def _format_repr_exception(exc: BaseException, obj: Any) -> str: +def _format_repr_exception(exc: BaseException, obj: object) -> str: try: exc_info = _try_repr_or_str(exc) except (KeyboardInterrupt, SystemExit): @@ -42,7 +45,7 @@ def __init__(self, maxsize: int) -> None: self.maxstring = maxsize self.maxsize = maxsize - def repr(self, x: Any) -> str: + def repr(self, x: object) -> str: try: s = super().repr(x) except (KeyboardInterrupt, SystemExit): @@ -51,7 +54,7 @@ def repr(self, x: Any) -> str: s = _format_repr_exception(exc, x) return _ellipsize(s, self.maxsize) - def repr_instance(self, x: Any, level: int) -> str: + def repr_instance(self, x: object, level: int) -> str: try: s = repr(x) except (KeyboardInterrupt, SystemExit): @@ -61,7 +64,7 @@ def repr_instance(self, x: Any, level: int) -> str: return _ellipsize(s, self.maxsize) -def safeformat(obj: Any) -> str: +def safeformat(obj: object) -> str: """return a pretty printed string for the given object. Failing __repr__ functions of user instances will be represented with a short exception info. @@ -72,7 +75,7 @@ def safeformat(obj: Any) -> str: return _format_repr_exception(exc, obj) -def saferepr(obj: Any, maxsize: int = 240) -> str: +def saferepr(obj: object, maxsize: int = 240) -> str: """return a size-limited safe repr-string for the given object. Failing __repr__ functions of user instances will be represented with a short exception info and 'saferepr' generally takes @@ -85,19 +88,39 @@ def saferepr(obj: Any, maxsize: int = 240) -> str: class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter): """PrettyPrinter that always dispatches (regardless of width).""" - def _format(self, object, stream, indent, allowance, context, level): - p = self._dispatch.get(type(object).__repr__, None) + def _format( + self, + object: object, + stream: IO[str], + indent: int, + allowance: int, + context: Dict[int, Any], + level: int, + ) -> None: + # Type ignored because _dispatch is private. + p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined] # noqa: F821 objid = id(object) if objid in context or p is None: - return super()._format(object, stream, indent, allowance, context, level) + # Type ignored because _format is private. + super()._format( # type: ignore[misc] # noqa: F821 + object, stream, indent, allowance, context, level, + ) + return context[objid] = 1 p(self, object, stream, indent, allowance, context, level + 1) del context[objid] -def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False): +def _pformat_dispatch( + object: object, + indent: int = 1, + width: int = 80, + depth: Optional[int] = None, + *, + compact: bool = False +) -> str: return AlwaysDispatchingPrettyPrinter( indent=indent, width=width, depth=depth, compact=compact ).pformat(object) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 997c1792192..6504db5744c 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -3,8 +3,8 @@ """ import sys from typing import Any -from typing import List from typing import Generator +from typing import List from typing import Optional from _pytest.assertion import rewrite From d95132178c073debcb687075f0f986d7d0322e9d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:15 +0300 Subject: [PATCH 305/823] Type annotate _pytest.assertion --- src/_pytest/assertion/__init__.py | 10 +- src/_pytest/assertion/rewrite.py | 149 +++++++++++++++++++----------- src/_pytest/assertion/truncate.py | 21 ++++- testing/test_assertrewrite.py | 5 +- 4 files changed, 120 insertions(+), 65 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 6504db5744c..f404607c1d0 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -46,7 +46,7 @@ def pytest_addoption(parser: Parser) -> None: ) -def register_assert_rewrite(*names) -> None: +def register_assert_rewrite(*names: str) -> None: """Register one or more module names to be rewritten on import. This function will make sure that this module or all modules inside @@ -75,27 +75,27 @@ def register_assert_rewrite(*names) -> None: class DummyRewriteHook: """A no-op import hook for when rewriting is disabled.""" - def mark_rewrite(self, *names): + def mark_rewrite(self, *names: str) -> None: pass class AssertionState: """State for the assertion plugin.""" - def __init__(self, config, mode): + def __init__(self, config: Config, mode) -> None: self.mode = mode self.trace = config.trace.root.get("assertion") self.hook = None # type: Optional[rewrite.AssertionRewritingHook] -def install_importhook(config): +def install_importhook(config: Config) -> rewrite.AssertionRewritingHook: """Try to install the rewrite hook, raise SystemError if it fails.""" config._store[assertstate_key] = AssertionState(config, "rewrite") config._store[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config) sys.meta_path.insert(0, hook) config._store[assertstate_key].trace("installed rewrite import hook") - def undo(): + def undo() -> None: hook = config._store[assertstate_key].hook if hook is not None and hook in sys.meta_path: sys.meta_path.remove(hook) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index bd4ea022c8d..cec0c550195 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -13,11 +13,15 @@ import sys import tokenize import types +from typing import Callable from typing import Dict +from typing import IO from typing import List from typing import Optional +from typing import Sequence from typing import Set from typing import Tuple +from typing import Union from _pytest._io.saferepr import saferepr from _pytest._version import version @@ -27,6 +31,8 @@ ) from _pytest.compat import fspath from _pytest.compat import TYPE_CHECKING +from _pytest.config import Config +from _pytest.main import Session from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import Path from _pytest.pathlib import PurePath @@ -48,13 +54,13 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader): """PEP302/PEP451 import hook which rewrites asserts.""" - def __init__(self, config): + def __init__(self, config: Config) -> None: self.config = config try: self.fnpats = config.getini("python_files") except ValueError: self.fnpats = ["test_*.py", "*_test.py"] - self.session = None + self.session = None # type: Optional[Session] self._rewritten_names = set() # type: Set[str] self._must_rewrite = set() # type: Set[str] # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, @@ -64,14 +70,19 @@ def __init__(self, config): self._marked_for_rewrite_cache = {} # type: Dict[str, bool] self._session_paths_checked = False - def set_session(self, session): + def set_session(self, session: Optional[Session]) -> None: self.session = session self._session_paths_checked = False # Indirection so we can mock calls to find_spec originated from the hook during testing _find_spec = importlib.machinery.PathFinder.find_spec - def find_spec(self, name, path=None, target=None): + def find_spec( + self, + name: str, + path: Optional[Sequence[Union[str, bytes]]] = None, + target: Optional[types.ModuleType] = None, + ) -> Optional[importlib.machinery.ModuleSpec]: if self._writing_pyc: return None state = self.config._store[assertstate_key] @@ -79,7 +90,8 @@ def find_spec(self, name, path=None, target=None): return None state.trace("find_module called for: %s" % name) - spec = self._find_spec(name, path) + # Type ignored because mypy is confused about the `self` binding here. + spec = self._find_spec(name, path) # type: ignore if ( # the import machinery could not find a file to import spec is None @@ -108,10 +120,14 @@ def find_spec(self, name, path=None, target=None): submodule_search_locations=spec.submodule_search_locations, ) - def create_module(self, spec): + def create_module( + self, spec: importlib.machinery.ModuleSpec + ) -> Optional[types.ModuleType]: return None # default behaviour is fine - def exec_module(self, module): + def exec_module(self, module: types.ModuleType) -> None: + assert module.__spec__ is not None + assert module.__spec__.origin is not None fn = Path(module.__spec__.origin) state = self.config._store[assertstate_key] @@ -151,7 +167,7 @@ def exec_module(self, module): state.trace("found cached rewritten pyc for {}".format(fn)) exec(co, module.__dict__) - def _early_rewrite_bailout(self, name, state): + def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool: """This is a fast way to get out of rewriting modules. Profiling has shown that the call to PathFinder.find_spec (inside of @@ -190,7 +206,7 @@ def _early_rewrite_bailout(self, name, state): state.trace("early skip of rewriting module: {}".format(name)) return True - def _should_rewrite(self, name, fn, state): + def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool: # always rewrite conftest files if os.path.basename(fn) == "conftest.py": state.trace("rewriting conftest file: {!r}".format(fn)) @@ -213,7 +229,7 @@ def _should_rewrite(self, name, fn, state): return self._is_marked_for_rewrite(name, state) - def _is_marked_for_rewrite(self, name: str, state): + def _is_marked_for_rewrite(self, name: str, state: "AssertionState") -> bool: try: return self._marked_for_rewrite_cache[name] except KeyError: @@ -246,7 +262,7 @@ def mark_rewrite(self, *names: str) -> None: self._must_rewrite.update(names) self._marked_for_rewrite_cache.clear() - def _warn_already_imported(self, name): + def _warn_already_imported(self, name: str) -> None: from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warnings import _issue_warning_captured @@ -258,13 +274,15 @@ def _warn_already_imported(self, name): stacklevel=5, ) - def get_data(self, pathname): + def get_data(self, pathname: Union[str, bytes]) -> bytes: """Optional PEP302 get_data API.""" with open(pathname, "rb") as f: return f.read() -def _write_pyc_fp(fp, source_stat, co): +def _write_pyc_fp( + fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType +) -> None: # Technically, we don't have to have the same pyc format as # (C)Python, since these "pycs" should never be seen by builtin # import. However, there's little reason deviate. @@ -280,7 +298,12 @@ def _write_pyc_fp(fp, source_stat, co): if sys.platform == "win32": from atomicwrites import atomic_write - def _write_pyc(state, co, source_stat, pyc): + def _write_pyc( + state: "AssertionState", + co: types.CodeType, + source_stat: os.stat_result, + pyc: Path, + ) -> bool: try: with atomic_write(fspath(pyc), mode="wb", overwrite=True) as fp: _write_pyc_fp(fp, source_stat, co) @@ -295,7 +318,12 @@ def _write_pyc(state, co, source_stat, pyc): else: - def _write_pyc(state, co, source_stat, pyc): + def _write_pyc( + state: "AssertionState", + co: types.CodeType, + source_stat: os.stat_result, + pyc: Path, + ) -> bool: proc_pyc = "{}.{}".format(pyc, os.getpid()) try: fp = open(proc_pyc, "wb") @@ -319,19 +347,21 @@ def _write_pyc(state, co, source_stat, pyc): return True -def _rewrite_test(fn, config): +def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]: """read and rewrite *fn* and return the code object.""" - fn = fspath(fn) - stat = os.stat(fn) - with open(fn, "rb") as f: + fn_ = fspath(fn) + stat = os.stat(fn_) + with open(fn_, "rb") as f: source = f.read() - tree = ast.parse(source, filename=fn) - rewrite_asserts(tree, source, fn, config) - co = compile(tree, fn, "exec", dont_inherit=True) + tree = ast.parse(source, filename=fn_) + rewrite_asserts(tree, source, fn_, config) + co = compile(tree, fn_, "exec", dont_inherit=True) return stat, co -def _read_pyc(source, pyc, trace=lambda x: None): +def _read_pyc( + source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None +) -> Optional[types.CodeType]: """Possibly read a pytest pyc containing rewritten code. Return rewritten code if successful or None if not. @@ -368,12 +398,17 @@ def _read_pyc(source, pyc, trace=lambda x: None): return co -def rewrite_asserts(mod, source, module_path=None, config=None): +def rewrite_asserts( + mod: ast.Module, + source: bytes, + module_path: Optional[str] = None, + config: Optional[Config] = None, +) -> None: """Rewrite the assert statements in mod.""" AssertionRewriter(module_path, config, source).run(mod) -def _saferepr(obj): +def _saferepr(obj: object) -> str: """Get a safe repr of an object for assertion error messages. The assertion formatting (util.format_explanation()) requires @@ -387,7 +422,7 @@ def _saferepr(obj): return saferepr(obj).replace("\n", "\\n") -def _format_assertmsg(obj): +def _format_assertmsg(obj: object) -> str: """Format the custom assertion message given. For strings this simply replaces newlines with '\n~' so that @@ -410,7 +445,7 @@ def _format_assertmsg(obj): return obj -def _should_repr_global_name(obj): +def _should_repr_global_name(obj: object) -> bool: if callable(obj): return False @@ -420,7 +455,7 @@ def _should_repr_global_name(obj): return True -def _format_boolop(explanations, is_or): +def _format_boolop(explanations, is_or: bool): explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")" if isinstance(explanation, str): return explanation.replace("%", "%%") @@ -428,8 +463,12 @@ def _format_boolop(explanations, is_or): return explanation.replace(b"%", b"%%") -def _call_reprcompare(ops, results, expls, each_obj): - # type: (Tuple[str, ...], Tuple[bool, ...], Tuple[str, ...], Tuple[object, ...]) -> str +def _call_reprcompare( + ops: Sequence[str], + results: Sequence[bool], + expls: Sequence[str], + each_obj: Sequence[object], +) -> str: for i, res, expl in zip(range(len(ops)), results, expls): try: done = not res @@ -607,7 +646,9 @@ class AssertionRewriter(ast.NodeVisitor): """ - def __init__(self, module_path, config, source): + def __init__( + self, module_path: Optional[str], config: Optional[Config], source: bytes + ) -> None: super().__init__() self.module_path = module_path self.config = config @@ -620,7 +661,7 @@ def __init__(self, module_path, config, source): self.source = source @functools.lru_cache(maxsize=1) - def _assert_expr_to_lineno(self): + def _assert_expr_to_lineno(self) -> Dict[int, str]: return _get_assertion_exprs(self.source) def run(self, mod: ast.Module) -> None: @@ -689,38 +730,38 @@ def run(self, mod: ast.Module) -> None: nodes.append(field) @staticmethod - def is_rewrite_disabled(docstring): + def is_rewrite_disabled(docstring: str) -> bool: return "PYTEST_DONT_REWRITE" in docstring - def variable(self): + def variable(self) -> str: """Get a new variable.""" # Use a character invalid in python identifiers to avoid clashing. name = "@py_assert" + str(next(self.variable_counter)) self.variables.append(name) return name - def assign(self, expr): + def assign(self, expr: ast.expr) -> ast.Name: """Give *expr* a name.""" name = self.variable() self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr)) return ast.Name(name, ast.Load()) - def display(self, expr): + def display(self, expr: ast.expr) -> ast.expr: """Call saferepr on the expression.""" return self.helper("_saferepr", expr) - def helper(self, name, *args): + def helper(self, name: str, *args: ast.expr) -> ast.expr: """Call a helper in this module.""" py_name = ast.Name("@pytest_ar", ast.Load()) attr = ast.Attribute(py_name, name, ast.Load()) return ast.Call(attr, list(args), []) - def builtin(self, name): + def builtin(self, name: str) -> ast.Attribute: """Return the builtin called *name*.""" builtin_name = ast.Name("@py_builtins", ast.Load()) return ast.Attribute(builtin_name, name, ast.Load()) - def explanation_param(self, expr): + def explanation_param(self, expr: ast.expr) -> str: """Return a new named %-formatting placeholder for expr. This creates a %-formatting placeholder for expr in the @@ -733,7 +774,7 @@ def explanation_param(self, expr): self.explanation_specifiers[specifier] = expr return "%(" + specifier + ")s" - def push_format_context(self): + def push_format_context(self) -> None: """Create a new formatting context. The format context is used for when an explanation wants to @@ -747,10 +788,10 @@ def push_format_context(self): self.explanation_specifiers = {} # type: Dict[str, ast.expr] self.stack.append(self.explanation_specifiers) - def pop_format_context(self, expl_expr): + def pop_format_context(self, expl_expr: ast.expr) -> ast.Name: """Format the %-formatted string with current format context. - The expl_expr should be an ast.Str instance constructed from + The expl_expr should be an str ast.expr instance constructed from the %-placeholders created by .explanation_param(). This will add the required code to format said string to .expl_stmts and return the ast.Name instance of the formatted string. @@ -768,13 +809,13 @@ def pop_format_context(self, expl_expr): self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form)) return ast.Name(name, ast.Load()) - def generic_visit(self, node): + def generic_visit(self, node: ast.AST) -> Tuple[ast.Name, str]: """Handle expressions we don't have custom code for.""" assert isinstance(node, ast.expr) res = self.assign(node) return res, self.explanation_param(self.display(res)) - def visit_Assert(self, assert_): + def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]: """Return the AST statements to replace the ast.Assert instance. This rewrites the test of an assertion to provide @@ -787,6 +828,8 @@ def visit_Assert(self, assert_): from _pytest.warning_types import PytestAssertRewriteWarning import warnings + # TODO: This assert should not be needed. + assert self.module_path is not None warnings.warn_explicit( PytestAssertRewriteWarning( "assertion is always true, perhaps remove parentheses?" @@ -889,7 +932,7 @@ def visit_Assert(self, assert_): set_location(stmt, assert_.lineno, assert_.col_offset) return self.statements - def visit_Name(self, name): + 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"), [], []) @@ -899,7 +942,7 @@ def visit_Name(self, name): expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) return name, self.explanation_param(expr) - def visit_BoolOp(self, boolop): + def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: res_var = self.variable() expl_list = self.assign(ast.List([], ast.Load())) app = ast.Attribute(expl_list, "append", ast.Load()) @@ -934,13 +977,13 @@ def visit_BoolOp(self, boolop): expl = self.pop_format_context(expl_template) return ast.Name(res_var, ast.Load()), self.explanation_param(expl) - def visit_UnaryOp(self, unary): + def visit_UnaryOp(self, unary: ast.UnaryOp) -> Tuple[ast.Name, str]: pattern = UNARY_MAP[unary.op.__class__] operand_res, operand_expl = self.visit(unary.operand) res = self.assign(ast.UnaryOp(unary.op, operand_res)) return res, pattern % (operand_expl,) - def visit_BinOp(self, binop): + def visit_BinOp(self, binop: ast.BinOp) -> Tuple[ast.Name, str]: symbol = BINOP_MAP[binop.op.__class__] left_expr, left_expl = self.visit(binop.left) right_expr, right_expl = self.visit(binop.right) @@ -948,7 +991,7 @@ def visit_BinOp(self, binop): res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) return res, explanation - def visit_Call(self, call): + def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]: """ visit `ast.Call` nodes """ @@ -975,13 +1018,13 @@ def visit_Call(self, call): outer_expl = "{}\n{{{} = {}\n}}".format(res_expl, res_expl, expl) return res, outer_expl - def visit_Starred(self, starred): + def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]: # From Python 3.5, a Starred node can appear in a function call res, expl = self.visit(starred.value) new_starred = ast.Starred(res, starred.ctx) return new_starred, "*" + expl - def visit_Attribute(self, attr): + def visit_Attribute(self, attr: ast.Attribute) -> Tuple[ast.Name, str]: if not isinstance(attr.ctx, ast.Load): return self.generic_visit(attr) value, value_expl = self.visit(attr.value) @@ -991,7 +1034,7 @@ def visit_Attribute(self, attr): expl = pat % (res_expl, res_expl, value_expl, attr.attr) return res, expl - def visit_Compare(self, comp: ast.Compare): + def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: self.push_format_context() left_res, left_expl = self.visit(comp.left) if isinstance(comp.left, (ast.Compare, ast.BoolOp)): @@ -1030,7 +1073,7 @@ def visit_Compare(self, comp: ast.Compare): return res, self.explanation_param(self.pop_format_context(expl_call)) -def try_makedirs(cache_dir) -> bool: +def try_makedirs(cache_dir: Path) -> bool: """Attempts to create the given directory and sub-directories exist, returns True if successful or it already exists""" try: diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index d97b05b441e..fb2bf9c8e35 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -5,13 +5,20 @@ ~8 terminal lines, unless running in "-vv" mode or running on CI. """ import os +from typing import List +from typing import Optional + +from _pytest.nodes import Item + DEFAULT_MAX_LINES = 8 DEFAULT_MAX_CHARS = 8 * 80 USAGE_MSG = "use '-vv' to show" -def truncate_if_required(explanation, item, max_length=None): +def truncate_if_required( + explanation: List[str], item: Item, max_length: Optional[int] = None +) -> List[str]: """ Truncate this assertion explanation if the given test item is eligible. """ @@ -20,7 +27,7 @@ def truncate_if_required(explanation, item, max_length=None): return explanation -def _should_truncate_item(item): +def _should_truncate_item(item: Item) -> bool: """ Whether or not this test item is eligible for truncation. """ @@ -28,13 +35,17 @@ def _should_truncate_item(item): return verbose < 2 and not _running_on_ci() -def _running_on_ci(): +def _running_on_ci() -> bool: """Check if we're currently running on a CI system.""" env_vars = ["CI", "BUILD_NUMBER"] return any(var in os.environ for var in env_vars) -def _truncate_explanation(input_lines, max_lines=None, max_chars=None): +def _truncate_explanation( + input_lines: List[str], + max_lines: Optional[int] = None, + max_chars: Optional[int] = None, +) -> List[str]: """ Truncate given list of strings that makes up the assertion explanation. @@ -73,7 +84,7 @@ def _truncate_explanation(input_lines, max_lines=None, max_chars=None): return truncated_explanation -def _truncate_by_char_count(input_lines, max_chars): +def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]: # Check if truncation required if len("".join(input_lines)) <= max_chars: return input_lines diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 7bc853e829e..212c631ef59 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -952,7 +952,8 @@ def test_write_pyc(self, testdir: Testdir, tmpdir, monkeypatch) -> None: state = AssertionState(config, "rewrite") source_path = str(tmpdir.ensure("source.py")) pycpath = tmpdir.join("pyc").strpath - assert _write_pyc(state, [1], os.stat(source_path), pycpath) + co = compile("1", "f.py", "single") + assert _write_pyc(state, co, os.stat(source_path), pycpath) if sys.platform == "win32": from contextlib import contextmanager @@ -974,7 +975,7 @@ def raise_oserror(*args): monkeypatch.setattr("os.rename", raise_oserror) - assert not _write_pyc(state, [1], os.stat(source_path), pycpath) + assert not _write_pyc(state, co, os.stat(source_path), pycpath) def test_resources_provider_for_loader(self, testdir): """ From e68a26199cb2bae0f001ab495232525f38227ad9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 306/823] Type annotate misc functions --- src/_pytest/cacheprovider.py | 60 ++++++++++++++++++++++-------------- src/_pytest/fixtures.py | 10 +++--- src/_pytest/hookspec.py | 4 ++- src/_pytest/main.py | 8 ++--- src/_pytest/mark/__init__.py | 7 +++-- src/_pytest/pathlib.py | 2 +- src/_pytest/pytester.py | 8 ++--- src/_pytest/stepwise.py | 18 ++++++++--- 8 files changed, 73 insertions(+), 44 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index c95f171527f..bb08c5a6e80 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -8,6 +8,7 @@ import os from typing import Dict from typing import Generator +from typing import Iterable from typing import List from typing import Optional from typing import Set @@ -27,10 +28,12 @@ from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.python import Module from _pytest.reports import TestReport + README_CONTENT = """\ # pytest cache directory # @@ -52,8 +55,8 @@ @attr.s class Cache: - _cachedir = attr.ib(repr=False) - _config = attr.ib(repr=False) + _cachedir = attr.ib(type=Path, repr=False) + _config = attr.ib(type=Config, repr=False) # sub-directory under cache-dir for directories created by "makedir" _CACHE_PREFIX_DIRS = "d" @@ -62,14 +65,14 @@ class Cache: _CACHE_PREFIX_VALUES = "v" @classmethod - def for_config(cls, config): + def for_config(cls, config: Config) -> "Cache": cachedir = cls.cache_dir_from_config(config) if config.getoption("cacheclear") and cachedir.is_dir(): cls.clear_cache(cachedir) return cls(cachedir, config) @classmethod - def clear_cache(cls, cachedir: Path): + def clear_cache(cls, cachedir: Path) -> None: """Clears the sub-directories used to hold cached directories and values.""" for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES): d = cachedir / prefix @@ -77,10 +80,10 @@ def clear_cache(cls, cachedir: Path): rm_rf(d) @staticmethod - def cache_dir_from_config(config): + def cache_dir_from_config(config: Config): return resolve_from_str(config.getini("cache_dir"), config.rootdir) - def warn(self, fmt, **args): + def warn(self, fmt: str, **args: object) -> None: import warnings from _pytest.warning_types import PytestCacheWarning @@ -90,7 +93,7 @@ def warn(self, fmt, **args): stacklevel=3, ) - def makedir(self, name): + def makedir(self, name: str) -> py.path.local: """ return a directory path object with the given name. If the directory does not yet exist, it will be created. You can use it to manage files likes e. g. store/retrieve database @@ -100,14 +103,14 @@ def makedir(self, name): Make sure the name contains your plugin or application identifiers to prevent clashes with other cache users. """ - name = Path(name) - if len(name.parts) > 1: + path = Path(name) + if len(path.parts) > 1: raise ValueError("name is not allowed to contain path separators") - res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, name) + res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path) res.mkdir(exist_ok=True, parents=True) return py.path.local(res) - def _getvaluepath(self, key): + def _getvaluepath(self, key: str) -> Path: return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key)) def get(self, key, default): @@ -128,7 +131,7 @@ def get(self, key, default): except (ValueError, OSError): return default - def set(self, key, value): + def set(self, key, value) -> None: """ save value for the given key. :param key: must be a ``/`` separated value. Usually the first @@ -158,7 +161,7 @@ def set(self, key, value): with f: f.write(data) - def _ensure_supporting_files(self): + def _ensure_supporting_files(self) -> None: """Create supporting files in the cache dir that are not really part of the cache.""" readme_path = self._cachedir / "README.md" readme_path.write_text(README_CONTENT) @@ -172,12 +175,12 @@ def _ensure_supporting_files(self): class LFPluginCollWrapper: - def __init__(self, lfplugin: "LFPlugin"): + def __init__(self, lfplugin: "LFPlugin") -> None: self.lfplugin = lfplugin self._collected_at_least_one_failure = False @pytest.hookimpl(hookwrapper=True) - def pytest_make_collect_report(self, collector) -> Generator: + def pytest_make_collect_report(self, collector: nodes.Collector) -> Generator: if isinstance(collector, Session): out = yield res = out.get_result() # type: CollectReport @@ -220,11 +223,13 @@ def pytest_make_collect_report(self, collector) -> Generator: class LFPluginCollSkipfiles: - def __init__(self, lfplugin: "LFPlugin"): + def __init__(self, lfplugin: "LFPlugin") -> None: self.lfplugin = lfplugin @pytest.hookimpl - def pytest_make_collect_report(self, collector) -> Optional[CollectReport]: + def pytest_make_collect_report( + self, collector: nodes.Collector + ) -> Optional[CollectReport]: if isinstance(collector, Module): if Path(str(collector.fspath)) not in self.lfplugin._last_failed_paths: self.lfplugin._skipped_files += 1 @@ -262,9 +267,10 @@ def get_last_failed_paths(self) -> Set[Path]: result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed} return {x for x in result if x.exists()} - def pytest_report_collectionfinish(self): + def pytest_report_collectionfinish(self) -> Optional[str]: if self.active and self.config.getoption("verbose") >= 0: return "run-last-failure: %s" % self._report_status + return None def pytest_runtest_logreport(self, report: TestReport) -> None: if (report.when == "call" and report.passed) or report.skipped: @@ -347,9 +353,10 @@ def pytest_sessionfinish(self, session: Session) -> None: class NFPlugin: """ Plugin which implements the --nf (run new-first) option """ - def __init__(self, config): + def __init__(self, config: Config) -> None: self.config = config self.active = config.option.newfirst + assert config.cache is not None self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) @pytest.hookimpl(hookwrapper=True, tryfirst=True) @@ -374,7 +381,7 @@ def pytest_collection_modifyitems( else: self.cached_nodeids.update(item.nodeid for item in items) - def _get_increasing_order(self, items): + def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]: return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) def pytest_sessionfinish(self) -> None: @@ -384,6 +391,8 @@ def pytest_sessionfinish(self) -> None: if config.getoption("collectonly"): return + + assert config.cache is not None config.cache.set("cache/nodeids", sorted(self.cached_nodeids)) @@ -462,7 +471,7 @@ def pytest_configure(config: Config) -> None: @pytest.fixture -def cache(request): +def cache(request: FixtureRequest) -> Cache: """ Return a cache object that can persist state between testing sessions. @@ -474,12 +483,14 @@ def cache(request): Values can be any object handled by the json stdlib module. """ + assert request.config.cache is not None return request.config.cache -def pytest_report_header(config): +def pytest_report_header(config: Config) -> Optional[str]: """Display cachedir with --cache-show and if non-default.""" if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache": + assert config.cache is not None cachedir = config.cache._cachedir # TODO: evaluate generating upward relative paths # starting with .., ../.. if sensible @@ -489,11 +500,14 @@ def pytest_report_header(config): except ValueError: displaypath = cachedir return "cachedir: {}".format(displaypath) + return None -def cacheshow(config, session): +def cacheshow(config: Config, session: Session) -> int: from pprint import pformat + assert config.cache is not None + tw = TerminalWriter() tw.line("cachedir: " + str(config.cache._cachedir)) if not config.cache._cachedir.is_dir(): diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 4cd9a20ef64..b45392ba5ca 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -57,6 +57,7 @@ from _pytest import nodes from _pytest.main import Session from _pytest.python import Metafunc + from _pytest.python import CallSpec2 _Scope = Literal["session", "package", "module", "class", "function"] @@ -217,10 +218,11 @@ def get_parametrized_fixture_keys(item, scopenum): the specified scope. """ assert scopenum < scopenum_function # function try: - cs = item.callspec + callspec = item.callspec # type: ignore[attr-defined] # noqa: F821 except AttributeError: pass else: + cs = callspec # type: CallSpec2 # cs.indices.items() is random order of argnames. Need to # sort this so that different calls to # get_parametrized_fixture_keys will be deterministic. @@ -434,9 +436,9 @@ def _getnextfixturedef(self, argname: str) -> "FixtureDef": return fixturedefs[index] @property - def config(self): + def config(self) -> Config: """ the pytest config object associated with this request. """ - return self._pyfuncitem.config + return self._pyfuncitem.config # type: ignore[no-any-return] # noqa: F723 @scopeproperty() def function(self): @@ -1464,7 +1466,7 @@ def pytest_generate_tests(self, metafunc: "Metafunc") -> None: else: continue # will raise FixtureLookupError at setup time - def pytest_collection_modifyitems(self, items): + def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None: # separate parametrized setups items[:] = reorder_items(items) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index ccdb0bde930..c5d5bdedd4c 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -223,7 +223,9 @@ def pytest_collection(session: "Session") -> Optional[Any]: """ -def pytest_collection_modifyitems(session: "Session", config: "Config", items): +def pytest_collection_modifyitems( + session: "Session", config: "Config", items: List["Item"] +) -> None: """ called after collection has been performed, may filter or re-order the items in-place. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d891335a749..a80097f5afc 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -333,7 +333,7 @@ def pytest_ignore_collect( return None -def pytest_collection_modifyitems(items, config: Config) -> None: +def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> None: deselect_prefixes = tuple(config.getoption("deselect") or []) if not deselect_prefixes: return @@ -487,18 +487,18 @@ def perform_collect( # noqa: F811 @overload def _perform_collect( self, args: Optional[Sequence[str]], genitems: "Literal[True]" - ) -> Sequence[nodes.Item]: + ) -> List[nodes.Item]: raise NotImplementedError() @overload # noqa: F811 def _perform_collect( # noqa: F811 self, args: Optional[Sequence[str]], genitems: bool - ) -> Sequence[Union[nodes.Item, nodes.Collector]]: + ) -> Union[List[Union[nodes.Item]], List[Union[nodes.Item, nodes.Collector]]]: raise NotImplementedError() def _perform_collect( # noqa: F811 self, args: Optional[Sequence[str]], genitems: bool - ) -> Sequence[Union[nodes.Item, nodes.Collector]]: + ) -> Union[List[Union[nodes.Item]], List[Union[nodes.Item, nodes.Collector]]]: if args is None: args = self.config.args self.trace("perform_collect", self, args) diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 05afb7749ac..16e821aee9e 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -2,6 +2,7 @@ import typing import warnings from typing import AbstractSet +from typing import List from typing import Optional from typing import Union @@ -173,7 +174,7 @@ def __call__(self, subname: str) -> bool: return False -def deselect_by_keyword(items, config: Config) -> None: +def deselect_by_keyword(items: "List[Item]", config: Config) -> None: keywordexpr = config.option.keyword.lstrip() if not keywordexpr: return @@ -229,7 +230,7 @@ def __call__(self, name: str) -> bool: return name in self.own_mark_names -def deselect_by_mark(items, config: Config) -> None: +def deselect_by_mark(items: "List[Item]", config: Config) -> None: matchexpr = config.option.markexpr if not matchexpr: return @@ -254,7 +255,7 @@ def deselect_by_mark(items, config: Config) -> None: items[:] = remaining -def pytest_collection_modifyitems(items, config: Config) -> None: +def pytest_collection_modifyitems(items: "List[Item]", config: Config) -> None: deselect_by_keyword(items, config) deselect_by_mark(items, config) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 90a7460b029..6878965e0c5 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -348,7 +348,7 @@ def make_numbered_dir_with_cleanup( raise e -def resolve_from_str(input, root): +def resolve_from_str(input: str, root): assert not isinstance(input, Path), "would break on py2" root = Path(root) input = expanduser(input) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 12ad0591c4a..60df17b90ac 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -647,8 +647,8 @@ def to_text(s): for basename, value in items: p = self.tmpdir.join(basename).new(ext=ext) p.dirpath().ensure_dir() - source = Source(value) - source = "\n".join(to_text(line) for line in source.lines) + source_ = Source(value) + source = "\n".join(to_text(line) for line in source_.lines) p.write(source.strip().encode(encoding), "wb") if ret is None: ret = p @@ -839,7 +839,7 @@ def getpathnode(self, path): config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) return res - def genitems(self, colitems): + def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]: """Generate all test items from a collection node. This recurses into the collection node and returns a list of all the @@ -847,7 +847,7 @@ def genitems(self, colitems): """ session = colitems[0].session - result = [] + result = [] # type: List[Item] for colitem in colitems: result.extend(session.genitems(colitem)) return result diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 1921245dc18..85cbe293151 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,4 +1,8 @@ +from typing import List +from typing import Optional + import pytest +from _pytest import nodes from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.main import Session @@ -28,20 +32,23 @@ def pytest_configure(config: Config) -> None: class StepwisePlugin: - def __init__(self, config): + def __init__(self, config: Config) -> None: self.config = config self.active = config.getvalue("stepwise") - self.session = None + self.session = None # type: Optional[Session] self.report_status = "" if self.active: + assert config.cache is not None self.lastfailed = config.cache.get("cache/stepwise", None) self.skip = config.getvalue("stepwise_skip") def pytest_sessionstart(self, session: Session) -> None: self.session = session - def pytest_collection_modifyitems(self, session, config, items): + def pytest_collection_modifyitems( + self, session: Session, config: Config, items: List[nodes.Item] + ) -> None: if not self.active: return if not self.lastfailed: @@ -89,6 +96,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: else: # Mark test as the last failing and interrupt the test session. self.lastfailed = report.nodeid + assert self.session is not None self.session.shouldstop = ( "Test failed, continuing from this test next run." ) @@ -100,11 +108,13 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: if report.nodeid == self.lastfailed: self.lastfailed = None - def pytest_report_collectionfinish(self): + def pytest_report_collectionfinish(self) -> Optional[str]: if self.active and self.config.getoption("verbose") >= 0 and self.report_status: return "stepwise: %s" % self.report_status + return None def pytest_sessionfinish(self, session: Session) -> None: + assert self.config.cache is not None if self.active: self.config.cache.set("cache/stepwise", self.lastfailed) else: From 387d9d04f7173f25c0719610e07921ff19dc5b99 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 307/823] Type annotate tricky reorder_items() function in fixtures.py --- src/_pytest/fixtures.py | 60 ++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b45392ba5ca..1a917874353 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -50,6 +50,7 @@ from _pytest.outcomes import TEST_OUTCOME if TYPE_CHECKING: + from typing import Deque from typing import NoReturn from typing import Type from typing_extensions import Literal @@ -213,7 +214,11 @@ def getfixturemarker(obj): return None -def get_parametrized_fixture_keys(item, scopenum): +# Parametrized fixture key, helper alias for code below. +_Key = Tuple[object, ...] + + +def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator[_Key]: """ return list of keys for all parametrized arguments which match the specified scope. """ assert scopenum < scopenum_function # function @@ -230,13 +235,14 @@ def get_parametrized_fixture_keys(item, scopenum): if cs._arg2scopenum[argname] != scopenum: continue if scopenum == 0: # session - key = (argname, param_index) + key = (argname, param_index) # type: _Key elif scopenum == 1: # package key = (argname, param_index, item.fspath.dirpath()) elif scopenum == 2: # module key = (argname, param_index, item.fspath) elif scopenum == 3: # class - key = (argname, param_index, item.fspath, item.cls) + item_cls = item.cls # type: ignore[attr-defined] # noqa: F821 + key = (argname, param_index, item.fspath, item_cls) yield key @@ -246,47 +252,65 @@ def get_parametrized_fixture_keys(item, scopenum): # setups and teardowns -def reorder_items(items): - argkeys_cache = {} - items_by_argkey = {} +def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]": + argkeys_cache = {} # type: Dict[int, Dict[nodes.Item, Dict[_Key, None]]] + items_by_argkey = {} # type: Dict[int, Dict[_Key, Deque[nodes.Item]]] for scopenum in range(0, scopenum_function): - argkeys_cache[scopenum] = d = {} - items_by_argkey[scopenum] = item_d = defaultdict(deque) + d = {} # type: Dict[nodes.Item, Dict[_Key, None]] + argkeys_cache[scopenum] = d + item_d = defaultdict(deque) # type: Dict[_Key, Deque[nodes.Item]] + items_by_argkey[scopenum] = item_d for item in items: - keys = order_preserving_dict.fromkeys( - get_parametrized_fixture_keys(item, scopenum) + # cast is a workaround for https://github.com/python/typeshed/issues/3800. + keys = cast( + "Dict[_Key, None]", + order_preserving_dict.fromkeys( + get_parametrized_fixture_keys(item, scopenum), None + ), ) if keys: d[item] = keys for key in keys: item_d[key].append(item) - items = order_preserving_dict.fromkeys(items) - return list(reorder_items_atscope(items, argkeys_cache, items_by_argkey, 0)) + # cast is a workaround for https://github.com/python/typeshed/issues/3800. + items_dict = cast( + "Dict[nodes.Item, None]", order_preserving_dict.fromkeys(items, None) + ) + return list(reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, 0)) -def fix_cache_order(item, argkeys_cache, items_by_argkey) -> None: +def fix_cache_order( + item: "nodes.Item", + argkeys_cache: "Dict[int, Dict[nodes.Item, Dict[_Key, None]]]", + items_by_argkey: "Dict[int, Dict[_Key, Deque[nodes.Item]]]", +) -> None: for scopenum in range(0, scopenum_function): for key in argkeys_cache[scopenum].get(item, []): items_by_argkey[scopenum][key].appendleft(item) -def reorder_items_atscope(items, argkeys_cache, items_by_argkey, scopenum): +def reorder_items_atscope( + items: "Dict[nodes.Item, None]", + argkeys_cache: "Dict[int, Dict[nodes.Item, Dict[_Key, None]]]", + items_by_argkey: "Dict[int, Dict[_Key, Deque[nodes.Item]]]", + scopenum: int, +) -> "Dict[nodes.Item, None]": if scopenum >= scopenum_function or len(items) < 3: return items - ignore = set() + ignore = set() # type: Set[Optional[_Key]] items_deque = deque(items) - items_done = order_preserving_dict() + items_done = order_preserving_dict() # type: Dict[nodes.Item, None] scoped_items_by_argkey = items_by_argkey[scopenum] scoped_argkeys_cache = argkeys_cache[scopenum] while items_deque: - no_argkey_group = order_preserving_dict() + no_argkey_group = order_preserving_dict() # type: Dict[nodes.Item, None] slicing_argkey = None while items_deque: item = items_deque.popleft() if item in items_done or item in no_argkey_group: continue argkeys = order_preserving_dict.fromkeys( - k for k in scoped_argkeys_cache.get(item, []) if k not in ignore + (k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None ) if not argkeys: no_argkey_group[item] = None From 32dd0e87cb2e6750c1fc2356eb451c9811bdb065 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 308/823] Type annotate _pytest.doctest --- src/_pytest/doctest.py | 111 ++++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 34 deletions(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 026476b8aae..ab8085982af 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -4,12 +4,17 @@ import platform import sys import traceback +import types import warnings from contextlib import contextmanager +from typing import Any +from typing import Callable from typing import Dict +from typing import Generator from typing import Iterable from typing import List from typing import Optional +from typing import Pattern from typing import Sequence from typing import Tuple from typing import Union @@ -24,6 +29,7 @@ from _pytest._io import TerminalWriter from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING +from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureRequest from _pytest.outcomes import OutcomeException @@ -131,7 +137,7 @@ def _is_setup_py(path: py.path.local) -> bool: return b"setuptools" in contents or b"distutils" in contents -def _is_doctest(config, path, parent): +def _is_doctest(config: Config, path: py.path.local, parent) -> bool: if path.ext in (".txt", ".rst") and parent.session.isinitpath(path): return True globs = config.getoption("doctestglob") or ["test*.txt"] @@ -144,7 +150,7 @@ def _is_doctest(config, path, parent): class ReprFailDoctest(TerminalRepr): def __init__( self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]] - ): + ) -> None: self.reprlocation_lines = reprlocation_lines def toterminal(self, tw: TerminalWriter) -> None: @@ -155,7 +161,7 @@ def toterminal(self, tw: TerminalWriter) -> None: class MultipleDoctestFailures(Exception): - def __init__(self, failures): + def __init__(self, failures: "Sequence[doctest.DocTestFailure]") -> None: super().__init__() self.failures = failures @@ -170,21 +176,33 @@ class PytestDoctestRunner(doctest.DebugRunner): """ def __init__( - self, checker=None, verbose=None, optionflags=0, continue_on_failure=True - ): + self, + checker: Optional[doctest.OutputChecker] = None, + verbose: Optional[bool] = None, + optionflags: int = 0, + continue_on_failure: bool = True, + ) -> None: doctest.DebugRunner.__init__( self, checker=checker, verbose=verbose, optionflags=optionflags ) self.continue_on_failure = continue_on_failure - def report_failure(self, out, test, example, got): + def report_failure( + self, out, test: "doctest.DocTest", example: "doctest.Example", got: str, + ) -> None: failure = doctest.DocTestFailure(test, example, got) if self.continue_on_failure: out.append(failure) else: raise failure - def report_unexpected_exception(self, out, test, example, exc_info): + def report_unexpected_exception( + self, + out, + test: "doctest.DocTest", + example: "doctest.Example", + exc_info: "Tuple[Type[BaseException], BaseException, types.TracebackType]", + ) -> None: if isinstance(exc_info[1], OutcomeException): raise exc_info[1] if isinstance(exc_info[1], bdb.BdbQuit): @@ -219,16 +237,27 @@ def _get_runner( class DoctestItem(pytest.Item): - def __init__(self, name, parent, runner=None, dtest=None): + def __init__( + self, + name: str, + parent: "Union[DoctestTextfile, DoctestModule]", + runner: Optional["doctest.DocTestRunner"] = None, + dtest: Optional["doctest.DocTest"] = None, + ) -> None: super().__init__(name, parent) self.runner = runner self.dtest = dtest self.obj = None - self.fixture_request = None + self.fixture_request = None # type: Optional[FixtureRequest] @classmethod def from_parent( # type: ignore - cls, parent: "Union[DoctestTextfile, DoctestModule]", *, name, runner, dtest + cls, + parent: "Union[DoctestTextfile, DoctestModule]", + *, + name: str, + runner: "doctest.DocTestRunner", + dtest: "doctest.DocTest" ): # incompatible signature due to to imposed limits on sublcass """ @@ -236,7 +265,7 @@ def from_parent( # type: ignore """ return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) - def setup(self): + def setup(self) -> None: if self.dtest is not None: self.fixture_request = _setup_fixtures(self) globs = dict(getfixture=self.fixture_request.getfixturevalue) @@ -247,14 +276,18 @@ def setup(self): self.dtest.globs.update(globs) def runtest(self) -> None: + assert self.dtest is not None + assert self.runner is not None _check_all_skipped(self.dtest) self._disable_output_capturing_for_darwin() failures = [] # type: List[doctest.DocTestFailure] - self.runner.run(self.dtest, out=failures) + # Type ignored because we change the type of `out` from what + # doctest expects. + self.runner.run(self.dtest, out=failures) # type: ignore[arg-type] # noqa: F821 if failures: raise MultipleDoctestFailures(failures) - def _disable_output_capturing_for_darwin(self): + def _disable_output_capturing_for_darwin(self) -> None: """ Disable output capturing. Otherwise, stdout is lost to doctest (#985) """ @@ -272,10 +305,12 @@ def repr_failure(self, excinfo): failures = ( None - ) # type: Optional[List[Union[doctest.DocTestFailure, doctest.UnexpectedException]]] - if excinfo.errisinstance((doctest.DocTestFailure, doctest.UnexpectedException)): + ) # type: Optional[Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]] + if isinstance( + excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException) + ): failures = [excinfo.value] - elif excinfo.errisinstance(MultipleDoctestFailures): + elif isinstance(excinfo.value, MultipleDoctestFailures): failures = excinfo.value.failures if failures is not None: @@ -289,7 +324,8 @@ def repr_failure(self, excinfo): else: lineno = test.lineno + example.lineno + 1 message = type(failure).__name__ - reprlocation = ReprFileLocation(filename, lineno, message) + # TODO: ReprFileLocation doesn't expect a None lineno. + reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] # noqa: F821 checker = _get_checker() report_choice = _get_report_choice( self.config.getoption("doctestreport") @@ -329,7 +365,8 @@ def repr_failure(self, excinfo): else: return super().repr_failure(excinfo) - def reportinfo(self) -> Tuple[py.path.local, int, str]: + def reportinfo(self): + assert self.dtest is not None return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name @@ -399,7 +436,7 @@ def collect(self) -> Iterable[DoctestItem]: ) -def _check_all_skipped(test): +def _check_all_skipped(test: "doctest.DocTest") -> None: """raises pytest.skip() if all examples in the given DocTest have the SKIP option set. """ @@ -410,7 +447,7 @@ def _check_all_skipped(test): pytest.skip("all tests skipped by +SKIP option") -def _is_mocked(obj): +def _is_mocked(obj: object) -> bool: """ returns if a object is possibly a mock object by checking the existence of a highly improbable attribute """ @@ -421,23 +458,26 @@ def _is_mocked(obj): @contextmanager -def _patch_unwrap_mock_aware(): +def _patch_unwrap_mock_aware() -> Generator[None, None, None]: """ contextmanager which replaces ``inspect.unwrap`` with a version that's aware of mock objects and doesn't recurse on them """ real_unwrap = inspect.unwrap - def _mock_aware_unwrap(obj, stop=None): + def _mock_aware_unwrap( + func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None + ) -> Any: try: if stop is None or stop is _is_mocked: - return real_unwrap(obj, stop=_is_mocked) - return real_unwrap(obj, stop=lambda obj: _is_mocked(obj) or stop(obj)) + return real_unwrap(func, stop=_is_mocked) + _stop = stop + return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func)) except Exception as e: warnings.warn( "Got %r when unwrapping %r. This is usually caused " "by a violation of Python's object protocol; see e.g. " - "https://github.com/pytest-dev/pytest/issues/5080" % (e, obj), + "https://github.com/pytest-dev/pytest/issues/5080" % (e, func), PytestWarning, ) raise @@ -469,7 +509,10 @@ def _find_lineno(self, obj, source_lines): """ if isinstance(obj, property): obj = getattr(obj, "fget", obj) - return doctest.DocTestFinder._find_lineno(self, obj, source_lines) + # Type ignored because this is a private function. + return doctest.DocTestFinder._find_lineno( # type: ignore + self, obj, source_lines, + ) def _find( self, tests, obj, name, module, source_lines, globs, seen @@ -510,17 +553,17 @@ def _find( ) -def _setup_fixtures(doctest_item): +def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest: """ Used by DoctestTextfile and DoctestItem to setup fixture information. """ - def func(): + def func() -> None: pass - doctest_item.funcargs = {} + doctest_item.funcargs = {} # type: ignore[attr-defined] # noqa: F821 fm = doctest_item.session._fixturemanager - doctest_item._fixtureinfo = fm.getfixtureinfo( + doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined] # noqa: F821 node=doctest_item, func=func, cls=None, funcargs=False ) fixture_request = FixtureRequest(doctest_item) @@ -564,7 +607,7 @@ class LiteralsOutputChecker(doctest.OutputChecker): re.VERBOSE, ) - def check_output(self, want, got, optionflags): + def check_output(self, want: str, got: str, optionflags: int) -> bool: if doctest.OutputChecker.check_output(self, want, got, optionflags): return True @@ -575,7 +618,7 @@ def check_output(self, want, got, optionflags): if not allow_unicode and not allow_bytes and not allow_number: return False - def remove_prefixes(regex, txt): + def remove_prefixes(regex: Pattern[str], txt: str) -> str: return re.sub(regex, r"\1\2", txt) if allow_unicode: @@ -591,7 +634,7 @@ def remove_prefixes(regex, txt): return doctest.OutputChecker.check_output(self, want, got, optionflags) - def _remove_unwanted_precision(self, want, got): + def _remove_unwanted_precision(self, want: str, got: str) -> str: wants = list(self._number_re.finditer(want)) gots = list(self._number_re.finditer(got)) if len(wants) != len(gots): @@ -686,7 +729,7 @@ def _get_report_choice(key: str) -> int: @pytest.fixture(scope="session") -def doctest_namespace(): +def doctest_namespace() -> Dict[str, Any]: """ Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. """ From fc325bc0c3e5c8694ecf8e3b08770b4da47c59e9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 309/823] Type annotate more of _pytest.nodes --- src/_pytest/mark/__init__.py | 6 ++-- src/_pytest/nodes.py | 63 +++++++++++++++++++++++++++--------- src/_pytest/python.py | 6 ++-- 3 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 16e821aee9e..7bbea54d297 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -147,9 +147,9 @@ def from_item(cls, item: "Item") -> "KeywordMatcher": # Add the names of the current item and any parent items import pytest - for item in item.listchain(): - if not isinstance(item, (pytest.Instance, pytest.Session)): - mapped_names.add(item.name) + for node in item.listchain(): + if not isinstance(node, (pytest.Instance, pytest.Session)): + mapped_names.add(node.name) # Add the names added as extra keywords to current or parent items mapped_names.update(item.listextrakeywords()) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 010dce925e4..4fdf1df7435 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -5,11 +5,13 @@ from typing import Callable from typing import Dict from typing import Iterable +from typing import Iterator from typing import List from typing import Optional from typing import Sequence from typing import Set from typing import Tuple +from typing import TypeVar from typing import Union import py @@ -20,6 +22,7 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprExceptionInfo from _pytest.compat import cached_property +from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import ConftestImportFailure @@ -36,6 +39,8 @@ from _pytest.store import Store if TYPE_CHECKING: + from typing import Type + # Imported here due to circular import. from _pytest.main import Session @@ -45,7 +50,7 @@ @lru_cache(maxsize=None) -def _splitnode(nodeid): +def _splitnode(nodeid: str) -> Tuple[str, ...]: """Split a nodeid into constituent 'parts'. Node IDs are strings, and can be things like: @@ -70,7 +75,7 @@ def _splitnode(nodeid): return tuple(parts) -def ischildnode(baseid, nodeid): +def ischildnode(baseid: str, nodeid: str) -> bool: """Return True if the nodeid is a child node of the baseid. E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp' @@ -82,6 +87,9 @@ def ischildnode(baseid, nodeid): return node_parts[: len(base_parts)] == base_parts +_NodeType = TypeVar("_NodeType", bound="Node") + + class NodeMeta(type): def __call__(self, *k, **kw): warnings.warn(NODE_USE_FROM_PARENT.format(name=self.__name__), stacklevel=2) @@ -191,7 +199,7 @@ def ihook(self): """ fspath sensitive hook proxy used to call pytest hooks""" return self.session.gethookproxy(self.fspath) - def __repr__(self): + def __repr__(self) -> str: return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) def warn(self, warning): @@ -232,16 +240,16 @@ def nodeid(self) -> str: """ a ::-separated string denoting its collection tree address. """ return self._nodeid - def __hash__(self): + def __hash__(self) -> int: return hash(self._nodeid) - def setup(self): + def setup(self) -> None: pass - def teardown(self): + def teardown(self) -> None: pass - def listchain(self): + def listchain(self) -> List["Node"]: """ return list of all parent collectors up to self, starting from root of collection tree. """ chain = [] @@ -276,7 +284,7 @@ def add_marker( else: self.own_markers.insert(0, marker_.mark) - def iter_markers(self, name=None): + def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]: """ :param name: if given, filter the results by the name attribute @@ -284,7 +292,9 @@ def iter_markers(self, name=None): """ return (x[1] for x in self.iter_markers_with_node(name=name)) - def iter_markers_with_node(self, name=None): + def iter_markers_with_node( + self, name: Optional[str] = None + ) -> Iterator[Tuple["Node", Mark]]: """ :param name: if given, filter the results by the name attribute @@ -296,7 +306,17 @@ def iter_markers_with_node(self, name=None): if name is None or getattr(mark, "name", None) == name: yield node, mark - def get_closest_marker(self, name, default=None): + @overload + def get_closest_marker(self, name: str) -> Optional[Mark]: + raise NotImplementedError() + + @overload # noqa: F811 + def get_closest_marker(self, name: str, default: Mark) -> Mark: # noqa: F811 + raise NotImplementedError() + + def get_closest_marker( # noqa: F811 + self, name: str, default: Optional[Mark] = None + ) -> Optional[Mark]: """return the first marker matching the name, from closest (for example function) to farther level (for example module level). @@ -305,14 +325,14 @@ def get_closest_marker(self, name, default=None): """ return next(self.iter_markers(name=name), default) - def listextrakeywords(self): + def listextrakeywords(self) -> Set[str]: """ Return a set of all extra keywords in self and any parents.""" extra_keywords = set() # type: Set[str] for item in self.listchain(): extra_keywords.update(item.extra_keyword_matches) return extra_keywords - def listnames(self): + def listnames(self) -> List[str]: return [x.name for x in self.listchain()] def addfinalizer(self, fin: Callable[[], object]) -> None: @@ -323,12 +343,13 @@ def addfinalizer(self, fin: Callable[[], object]) -> None: """ self.session._setupstate.addfinalizer(fin, self) - def getparent(self, cls): + def getparent(self, cls: "Type[_NodeType]") -> Optional[_NodeType]: """ get the next parent node (including ourself) which is an instance of the given class""" current = self # type: Optional[Node] while current and not isinstance(current, cls): current = current.parent + assert current is None or isinstance(current, cls) return current def _prunetraceback(self, excinfo): @@ -479,7 +500,12 @@ def __getattr__(self, name: str): class FSCollector(Collector): def __init__( - self, fspath: py.path.local, parent=None, config=None, session=None, nodeid=None + self, + fspath: py.path.local, + parent=None, + config: Optional[Config] = None, + session: Optional["Session"] = None, + nodeid: Optional[str] = None, ) -> None: name = fspath.basename if parent is not None: @@ -579,7 +605,14 @@ class Item(Node): nextitem = None - def __init__(self, name, parent=None, config=None, session=None, nodeid=None): + def __init__( + self, + name, + parent=None, + config: Optional[Config] = None, + session: Optional["Session"] = None, + nodeid: Optional[str] = None, + ) -> None: super().__init__(name, parent, config, session, nodeid=nodeid) self._report_sections = [] # type: List[Tuple[str, str, str]] diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 9b8dcf60868..55ed2b164a7 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -423,7 +423,9 @@ def _makeitem( return item def _genfunctions(self, name, funcobj): - module = self.getparent(Module).obj + modulecol = self.getparent(Module) + assert modulecol is not None + module = modulecol.obj clscol = self.getparent(Class) cls = clscol and clscol.obj or None fm = self.session._fixturemanager @@ -437,7 +439,7 @@ def _genfunctions(self, name, funcobj): methods = [] if hasattr(module, "pytest_generate_tests"): methods.append(module.pytest_generate_tests) - if hasattr(cls, "pytest_generate_tests"): + if cls is not None and hasattr(cls, "pytest_generate_tests"): methods.append(cls().pytest_generate_tests) self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) From 709bcbf3c413850b7bf10634b2637292ddda331d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 310/823] Type annotate _pytest.mark.evaluate --- src/_pytest/mark/evaluate.py | 37 ++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/_pytest/mark/evaluate.py b/src/_pytest/mark/evaluate.py index c47174e7156..759191668aa 100644 --- a/src/_pytest/mark/evaluate.py +++ b/src/_pytest/mark/evaluate.py @@ -4,10 +4,14 @@ import traceback from typing import Any from typing import Dict +from typing import List +from typing import Optional from ..outcomes import fail from ..outcomes import TEST_OUTCOME +from .structures import Mark from _pytest.config import Config +from _pytest.nodes import Item from _pytest.store import StoreKey @@ -28,29 +32,29 @@ def cached_eval(config: Config, expr: str, d: Dict[str, object]) -> Any: class MarkEvaluator: - def __init__(self, item, name): + def __init__(self, item: Item, name: str) -> None: self.item = item - self._marks = None - self._mark = None + self._marks = None # type: Optional[List[Mark]] + self._mark = None # type: Optional[Mark] self._mark_name = name - def __bool__(self): + def __bool__(self) -> bool: # don't cache here to prevent staleness return bool(self._get_marks()) - def wasvalid(self): + def wasvalid(self) -> bool: return not hasattr(self, "exc") - def _get_marks(self): + def _get_marks(self) -> List[Mark]: return list(self.item.iter_markers(name=self._mark_name)) - def invalidraise(self, exc): + def invalidraise(self, exc) -> Optional[bool]: raises = self.get("raises") if not raises: - return + return None return not isinstance(exc, raises) - def istrue(self): + def istrue(self) -> bool: try: return self._istrue() except TEST_OUTCOME: @@ -69,25 +73,26 @@ def istrue(self): pytrace=False, ) - def _getglobals(self): + def _getglobals(self) -> Dict[str, object]: d = {"os": os, "sys": sys, "platform": platform, "config": self.item.config} if hasattr(self.item, "obj"): - d.update(self.item.obj.__globals__) + d.update(self.item.obj.__globals__) # type: ignore[attr-defined] # noqa: F821 return d - def _istrue(self): + def _istrue(self) -> bool: if hasattr(self, "result"): - return self.result + result = getattr(self, "result") # type: bool + return result self._marks = self._get_marks() if self._marks: self.result = False for mark in self._marks: self._mark = mark - if "condition" in mark.kwargs: - args = (mark.kwargs["condition"],) - else: + if "condition" not in mark.kwargs: args = mark.args + else: + args = (mark.kwargs["condition"],) for expr in args: self.expr = expr From 90e58f89615327d78a0c25d148321edb296ca982 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 311/823] Type annotate some parts related to runner & reports --- src/_pytest/cacheprovider.py | 2 +- src/_pytest/helpconfig.py | 3 +- src/_pytest/hookspec.py | 17 +++++--- src/_pytest/main.py | 4 +- src/_pytest/reports.py | 79 ++++++++++++++++++++++-------------- src/_pytest/resultlog.py | 5 ++- src/_pytest/runner.py | 49 ++++++++++++++-------- src/_pytest/skipping.py | 9 +++- src/_pytest/terminal.py | 15 +++---- src/_pytest/unittest.py | 7 ++-- testing/test_runner.py | 26 ++++++------ 11 files changed, 132 insertions(+), 84 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index bb08c5a6e80..af7d57a2490 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -278,7 +278,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: elif report.failed: self.lastfailed[report.nodeid] = True - def pytest_collectreport(self, report): + def pytest_collectreport(self, report: CollectReport) -> None: passed = report.outcome in ("passed", "skipped") if passed: if report.nodeid in self.lastfailed: diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index c2519c8afc7..06e0954cf08 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 List from typing import Optional from typing import Union @@ -235,7 +236,7 @@ def getpluginversioninfo(config): return lines -def pytest_report_header(config): +def pytest_report_header(config: Config) -> List[str]: lines = [] if config.option.debug or config.option.traceconfig: lines.append( diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index c5d5bdedd4c..99f646bd6aa 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -3,6 +3,7 @@ from typing import List from typing import Mapping from typing import Optional +from typing import Sequence from typing import Tuple from typing import Union @@ -284,7 +285,7 @@ def pytest_itemcollected(item): """ we just collected a test item. """ -def pytest_collectreport(report): +def pytest_collectreport(report: "CollectReport") -> None: """ collector finished collecting. """ @@ -430,7 +431,7 @@ def pytest_runtest_teardown(item: "Item", nextitem: "Optional[Item]") -> None: @hookspec(firstresult=True) -def pytest_runtest_makereport(item: "Item", call: "CallInfo") -> Optional[object]: +def pytest_runtest_makereport(item: "Item", call: "CallInfo[None]") -> Optional[object]: """ return a :py:class:`_pytest.runner.TestReport` object for the given :py:class:`pytest.Item <_pytest.main.Item>` and :py:class:`_pytest.runner.CallInfo`. @@ -444,7 +445,7 @@ def pytest_runtest_logreport(report: "TestReport") -> None: @hookspec(firstresult=True) -def pytest_report_to_serializable(config: "Config", report): +def pytest_report_to_serializable(config: "Config", report: "BaseReport"): """ Serializes the given report object into a data structure suitable for sending over the wire, e.g. converted to JSON. @@ -580,7 +581,9 @@ def pytest_assertion_pass(item, lineno: int, orig: str, expl: str) -> None: # ------------------------------------------------------------------------- -def pytest_report_header(config: "Config", startdir): +def pytest_report_header( + config: "Config", startdir: py.path.local +) -> Union[str, List[str]]: """ return a string or list of strings to be displayed as header info for terminal reporting. :param _pytest.config.Config config: pytest config object @@ -601,7 +604,9 @@ def pytest_report_header(config: "Config", startdir): """ -def pytest_report_collectionfinish(config: "Config", startdir, items): +def pytest_report_collectionfinish( + config: "Config", startdir: py.path.local, items: "Sequence[Item]" +) -> Union[str, List[str]]: """ .. versionadded:: 3.2 @@ -758,7 +763,7 @@ def pytest_keyboard_interrupt(excinfo): def pytest_exception_interact( - node: "Node", call: "CallInfo", report: "BaseReport" + node: "Node", call: "CallInfo[object]", report: "Union[CollectReport, TestReport]" ) -> None: """called when an exception was raised which can potentially be interactively handled. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index a80097f5afc..1c1cda18bdf 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -442,7 +442,9 @@ def pytest_collectstart(self) -> None: raise self.Interrupted(self.shouldstop) @hookimpl(tryfirst=True) - def pytest_runtest_logreport(self, report: TestReport) -> None: + def pytest_runtest_logreport( + self, report: Union[TestReport, CollectReport] + ) -> None: if report.failed and not hasattr(report, "wasxfail"): self.testsfailed += 1 maxfail = self.config.getvalue("maxfail") diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 9763cb4ade3..7462cea0b69 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,9 +1,12 @@ from io import StringIO from pprint import pprint from typing import Any +from typing import Iterable +from typing import Iterator from typing import List from typing import Optional from typing import Tuple +from typing import TypeVar from typing import Union import attr @@ -21,12 +24,17 @@ from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter from _pytest.compat import TYPE_CHECKING +from _pytest.config import Config from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.outcomes import skip from _pytest.pathlib import Path if TYPE_CHECKING: + from typing import NoReturn + from typing_extensions import Type + from typing_extensions import Literal + from _pytest.runner import CallInfo @@ -42,6 +50,9 @@ def getslaveinfoline(node): return s +_R = TypeVar("_R", bound="BaseReport") + + class BaseReport: when = None # type: Optional[str] location = None # type: Optional[Tuple[str, Optional[int], str]] @@ -74,13 +85,13 @@ def toterminal(self, out) -> None: except UnicodeEncodeError: out.line("") - def get_sections(self, prefix): + def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]: for name, content in self.sections: if name.startswith(prefix): yield prefix, content @property - def longreprtext(self): + def longreprtext(self) -> str: """ Read-only property that returns the full string representation of ``longrepr``. @@ -95,7 +106,7 @@ def longreprtext(self): return exc.strip() @property - def caplog(self): + def caplog(self) -> str: """Return captured log lines, if log capturing is enabled .. versionadded:: 3.5 @@ -105,7 +116,7 @@ def caplog(self): ) @property - def capstdout(self): + def capstdout(self) -> str: """Return captured text from stdout, if capturing is enabled .. versionadded:: 3.0 @@ -115,7 +126,7 @@ def capstdout(self): ) @property - def capstderr(self): + def capstderr(self) -> str: """Return captured text from stderr, if capturing is enabled .. versionadded:: 3.0 @@ -133,7 +144,7 @@ def fspath(self) -> str: return self.nodeid.split("::")[0] @property - def count_towards_summary(self): + def count_towards_summary(self) -> bool: """ **Experimental** @@ -148,7 +159,7 @@ def count_towards_summary(self): return True @property - def head_line(self): + def head_line(self) -> Optional[str]: """ **Experimental** @@ -168,8 +179,9 @@ def head_line(self): if self.location is not None: fspath, lineno, domain = self.location return domain + return None - def _get_verbose_word(self, config): + def _get_verbose_word(self, config: Config): _category, _short, verbose = config.hook.pytest_report_teststatus( report=self, config=config ) @@ -187,7 +199,7 @@ def _to_json(self): return _report_to_json(self) @classmethod - def _from_json(cls, reportdict): + def _from_json(cls: "Type[_R]", reportdict) -> _R: """ This was originally the serialize_report() function from xdist (ca03269). @@ -200,7 +212,9 @@ def _from_json(cls, reportdict): return cls(**kwargs) -def _report_unserialization_failure(type_name, report_class, reportdict): +def _report_unserialization_failure( + type_name: str, report_class: "Type[BaseReport]", reportdict +) -> "NoReturn": url = "https://github.com/pytest-dev/pytest/issues" stream = StringIO() pprint("-" * 100, stream=stream) @@ -221,15 +235,15 @@ class TestReport(BaseReport): def __init__( self, - nodeid, + nodeid: str, location: Tuple[str, Optional[int], str], keywords, - outcome, + outcome: "Literal['passed', 'failed', 'skipped']", longrepr, - when, - sections=(), - duration=0, - user_properties=None, + when: "Literal['setup', 'call', 'teardown']", + sections: Iterable[Tuple[str, str]] = (), + duration: float = 0, + user_properties: Optional[Iterable[Tuple[str, object]]] = None, **extra ) -> None: #: normalized collection node id @@ -268,23 +282,25 @@ def __init__( self.__dict__.update(extra) - def __repr__(self): + def __repr__(self) -> str: return "<{} {!r} when={!r} outcome={!r}>".format( self.__class__.__name__, self.nodeid, self.when, self.outcome ) @classmethod - def from_item_and_call(cls, item: Item, call: "CallInfo") -> "TestReport": + def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": """ Factory method to create and fill a TestReport with standard item and call info. """ when = call.when + # Remove "collect" from the Literal type -- only for collection calls. + assert when != "collect" duration = call.duration keywords = {x: 1 for x in item.keywords} excinfo = call.excinfo sections = [] if not call.excinfo: - outcome = "passed" + outcome = "passed" # type: Literal["passed", "failed", "skipped"] # TODO: Improve this Any. longrepr = None # type: Optional[Any] else: @@ -324,10 +340,10 @@ class CollectReport(BaseReport): def __init__( self, nodeid: str, - outcome, + outcome: "Literal['passed', 'skipped', 'failed']", longrepr, result: Optional[List[Union[Item, Collector]]], - sections=(), + sections: Iterable[Tuple[str, str]] = (), **extra ) -> None: self.nodeid = nodeid @@ -341,28 +357,29 @@ def __init__( def location(self): return (self.fspath, None, self.fspath) - def __repr__(self): + def __repr__(self) -> str: return "".format( self.nodeid, len(self.result), self.outcome ) class CollectErrorRepr(TerminalRepr): - def __init__(self, msg): + def __init__(self, msg) -> None: self.longrepr = msg def toterminal(self, out) -> None: out.line(self.longrepr, red=True) -def pytest_report_to_serializable(report): +def pytest_report_to_serializable(report: BaseReport): if isinstance(report, (TestReport, CollectReport)): data = report._to_json() data["$report_type"] = report.__class__.__name__ return data + return None -def pytest_report_from_serializable(data): +def pytest_report_from_serializable(data) -> Optional[BaseReport]: if "$report_type" in data: if data["$report_type"] == "TestReport": return TestReport._from_json(data) @@ -371,9 +388,10 @@ def pytest_report_from_serializable(data): assert False, "Unknown report_type unserialize data: {}".format( data["$report_type"] ) + return None -def _report_to_json(report): +def _report_to_json(report: BaseReport): """ This was originally the serialize_report() function from xdist (ca03269). @@ -381,11 +399,12 @@ def _report_to_json(report): serialization. """ - def serialize_repr_entry(entry): - entry_data = {"type": type(entry).__name__, "data": attr.asdict(entry)} - for key, value in entry_data["data"].items(): + def serialize_repr_entry(entry: Union[ReprEntry, ReprEntryNative]): + data = attr.asdict(entry) + for key, value in data.items(): if hasattr(value, "__dict__"): - entry_data["data"][key] = attr.asdict(value) + data[key] = attr.asdict(value) + entry_data = {"type": type(entry).__name__, "data": data} return entry_data def serialize_repr_traceback(reprtraceback: ReprTraceback): diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index 720ea9f490c..c2b0cf5563a 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -7,6 +7,7 @@ from _pytest.config import Config from _pytest.config.argparsing import Parser +from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.store import StoreKey @@ -87,7 +88,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: longrepr = str(report.longrepr) self.log_outcome(report, code, longrepr) - def pytest_collectreport(self, report): + def pytest_collectreport(self, report: CollectReport) -> None: if not report.passed: if report.failed: code = "F" @@ -95,7 +96,7 @@ def pytest_collectreport(self, report): else: assert report.skipped code = "S" - longrepr = "%s:%d: %s" % report.longrepr + longrepr = "%s:%d: %s" % report.longrepr # type: ignore self.log_outcome(report, code, longrepr) def pytest_internalerror(self, excrepr): diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 568065d948e..f89b673991f 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -3,10 +3,14 @@ import os import sys from typing import Callable +from typing import cast from typing import Dict +from typing import Generic from typing import List from typing import Optional from typing import Tuple +from typing import TypeVar +from typing import Union import attr @@ -179,7 +183,7 @@ def _update_current_test_var( os.environ.pop(var_name) -def pytest_report_teststatus(report): +def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: if report.when in ("setup", "teardown"): if report.failed: # category, shortletter, verbose-word @@ -188,6 +192,7 @@ def pytest_report_teststatus(report): return "skipped", "s", "SKIPPED" else: return "", "", "" + return None # @@ -217,9 +222,9 @@ def check_interactive_exception(call: "CallInfo", report: BaseReport) -> bool: def call_runtest_hook( item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds -) -> "CallInfo": +) -> "CallInfo[None]": if when == "setup": - ihook = item.ihook.pytest_runtest_setup + ihook = item.ihook.pytest_runtest_setup # type: Callable[..., None] elif when == "call": ihook = item.ihook.pytest_runtest_call elif when == "teardown": @@ -234,11 +239,14 @@ def call_runtest_hook( ) +_T = TypeVar("_T") + + @attr.s(repr=False) -class CallInfo: +class CallInfo(Generic[_T]): """ Result/Exception info a function invocation. - :param result: The return value of the call, if it didn't raise. Can only be accessed + :param T result: The return value of the call, if it didn't raise. Can only be accessed if excinfo is None. :param Optional[ExceptionInfo] excinfo: The captured exception of the call, if it raised. :param float start: The system time when the call started, in seconds since the epoch. @@ -247,28 +255,34 @@ class CallInfo: :param str when: The context of invocation: "setup", "call", "teardown", ... """ - _result = attr.ib() + _result = attr.ib(type="Optional[_T]") excinfo = attr.ib(type=Optional[ExceptionInfo]) start = attr.ib(type=float) stop = attr.ib(type=float) duration = attr.ib(type=float) - when = attr.ib(type=str) + when = attr.ib(type="Literal['collect', 'setup', 'call', 'teardown']") @property - def result(self): + def result(self) -> _T: if self.excinfo is not None: raise AttributeError("{!r} has no valid result".format(self)) - return self._result + # The cast is safe because an exception wasn't raised, hence + # _result has the expected function return type (which may be + # None, that's why a cast and not an assert). + return cast(_T, self._result) @classmethod - def from_call(cls, func, when, reraise=None) -> "CallInfo": - #: context of invocation: one of "setup", "call", - #: "teardown", "memocollect" + def from_call( + cls, + func: "Callable[[], _T]", + when: "Literal['collect', 'setup', 'call', 'teardown']", + reraise: "Optional[Union[Type[BaseException], Tuple[Type[BaseException], ...]]]" = None, + ) -> "CallInfo[_T]": excinfo = None start = timing.time() precise_start = timing.perf_counter() try: - result = func() + result = func() # type: Optional[_T] except BaseException: excinfo = ExceptionInfo.from_current() if reraise is not None and excinfo.errisinstance(reraise): @@ -293,7 +307,7 @@ def __repr__(self) -> str: return "".format(self.when, self.excinfo) -def pytest_runtest_makereport(item: Item, call: CallInfo) -> TestReport: +def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: return TestReport.from_item_and_call(item, call) @@ -301,7 +315,7 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: call = CallInfo.from_call(lambda: list(collector.collect()), "collect") longrepr = None if not call.excinfo: - outcome = "passed" + outcome = "passed" # type: Literal["passed", "skipped", "failed"] else: skip_exceptions = [Skipped] unittest = sys.modules.get("unittest") @@ -321,9 +335,8 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: if not hasattr(errorinfo, "toterminal"): errorinfo = CollectErrorRepr(errorinfo) longrepr = errorinfo - rep = CollectReport( - collector.nodeid, outcome, longrepr, getattr(call, "result", None) - ) + result = call.result if not call.excinfo else None + rep = CollectReport(collector.nodeid, outcome, longrepr, result) rep.call = call # type: ignore # see collect_one_node return rep diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 5994b5b2fa1..54621f111cc 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -1,4 +1,7 @@ """ support for skip/xfail functions and markers. """ +from typing import Optional +from typing import Tuple + from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser @@ -8,6 +11,7 @@ from _pytest.outcomes import skip from _pytest.outcomes import xfail from _pytest.python import Function +from _pytest.reports import BaseReport from _pytest.runner import CallInfo from _pytest.store import StoreKey @@ -129,7 +133,7 @@ def check_strict_xfail(pyfuncitem: Function) -> None: @hookimpl(hookwrapper=True) -def pytest_runtest_makereport(item: Item, call: CallInfo): +def pytest_runtest_makereport(item: Item, call: CallInfo[None]): outcome = yield rep = outcome.get_result() evalxfail = item._store.get(evalxfail_key, None) @@ -181,9 +185,10 @@ def pytest_runtest_makereport(item: Item, call: CallInfo): # called by terminalreporter progress reporting -def pytest_report_teststatus(report): +def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: if hasattr(report, "wasxfail"): if report.skipped: return "xfailed", "x", "XFAIL" elif report.passed: return "xpassed", "X", "XPASS" + return None diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index bc2b5bf2323..1b9601a2215 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -37,6 +37,7 @@ from _pytest.config import ExitCode from _pytest.config.argparsing import Parser from _pytest.deprecated import TERMINALWRITER_WRITER +from _pytest.reports import BaseReport from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -218,14 +219,14 @@ def getreportopt(config: Config) -> str: @pytest.hookimpl(trylast=True) # after _pytest.runner -def pytest_report_teststatus(report: TestReport) -> Tuple[str, str, str]: +def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]: letter = "F" if report.passed: letter = "." elif report.skipped: letter = "s" - outcome = report.outcome + outcome = report.outcome # type: str if report.when in ("collect", "setup", "teardown") and outcome == "failed": outcome = "error" letter = "E" @@ -364,7 +365,7 @@ def write_ensure_prefix(self, prefix, extra="", **kwargs): self._tw.write(extra, **kwargs) self.currentfspath = -2 - def ensure_newline(self): + def ensure_newline(self) -> None: if self.currentfspath: self._tw.line() self.currentfspath = None @@ -375,7 +376,7 @@ def write(self, content: str, *, flush: bool = False, **markup: bool) -> None: def flush(self) -> None: self._tw.flush() - def write_line(self, line, **markup): + def write_line(self, line: Union[str, bytes], **markup) -> None: if not isinstance(line, str): line = str(line, errors="replace") self.ensure_newline() @@ -642,12 +643,12 @@ def pytest_sessionstart(self, session: "Session") -> None: ) self._write_report_lines_from_hooks(lines) - def _write_report_lines_from_hooks(self, lines): + def _write_report_lines_from_hooks(self, lines) -> None: lines.reverse() for line in collapse(lines): self.write_line(line) - def pytest_report_header(self, config): + def pytest_report_header(self, config: Config) -> List[str]: line = "rootdir: %s" % config.rootdir if config.inifile: @@ -664,7 +665,7 @@ def pytest_report_header(self, config): result.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo))) return result - def pytest_collection_finish(self, session): + def pytest_collection_finish(self, session: "Session") -> None: self.report_collect(True) lines = self.config.hook.pytest_report_collectionfinish( diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 3fbf7c88dbe..f9eb6e71939 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -255,7 +255,7 @@ def _prunetraceback(self, excinfo): @hookimpl(tryfirst=True) -def pytest_runtest_makereport(item: Item, call: CallInfo) -> None: +def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: if isinstance(item, TestCaseFunction): if item._excinfo: call.excinfo = item._excinfo.pop(0) @@ -272,9 +272,10 @@ def pytest_runtest_makereport(item: Item, call: CallInfo) -> None: unittest.SkipTest # type: ignore[attr-defined] # noqa: F821 ) ): + excinfo = call.excinfo # let's substitute the excinfo with a pytest.skip one - call2 = CallInfo.from_call( - lambda: pytest.skip(str(call.excinfo.value)), call.when + call2 = CallInfo[None].from_call( + lambda: pytest.skip(str(excinfo.value)), call.when ) call.excinfo = call2.excinfo diff --git a/testing/test_runner.py b/testing/test_runner.py index be79b14fd10..9c19ded0e70 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -465,27 +465,27 @@ def test_report_extra_parameters(reporttype: "Type[reports.BaseReport]") -> None def test_callinfo() -> None: - ci = runner.CallInfo.from_call(lambda: 0, "123") - assert ci.when == "123" + ci = runner.CallInfo.from_call(lambda: 0, "collect") + assert ci.when == "collect" assert ci.result == 0 assert "result" in repr(ci) - assert repr(ci) == "" - assert str(ci) == "" + assert repr(ci) == "" + assert str(ci) == "" - ci = runner.CallInfo.from_call(lambda: 0 / 0, "123") - assert ci.when == "123" - assert not hasattr(ci, "result") - assert repr(ci) == "".format(ci.excinfo) - assert str(ci) == repr(ci) - assert ci.excinfo + ci2 = runner.CallInfo.from_call(lambda: 0 / 0, "collect") + assert ci2.when == "collect" + assert not hasattr(ci2, "result") + assert repr(ci2) == "".format(ci2.excinfo) + assert str(ci2) == repr(ci2) + assert ci2.excinfo # Newlines are escaped. def raise_assertion(): assert 0, "assert_msg" - ci = runner.CallInfo.from_call(raise_assertion, "call") - assert repr(ci) == "".format(ci.excinfo) - assert "\n" not in repr(ci) + ci3 = runner.CallInfo.from_call(raise_assertion, "call") + assert repr(ci3) == "".format(ci3.excinfo) + assert "\n" not in repr(ci3) # design question: do we want general hooks in python files? From db5292868478e5b1a47f23a7686c5995cabb3bf2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 312/823] Type annotate _pytest.logging --- src/_pytest/logging.py | 100 ++++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 41 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 92046ed51d6..ce3a18f032d 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -11,18 +11,24 @@ from typing import List from typing import Mapping from typing import Optional +from typing import Tuple +from typing import TypeVar from typing import Union import pytest from _pytest import nodes +from _pytest._io import TerminalWriter +from _pytest.capture import CaptureManager from _pytest.compat import nullcontext from _pytest.config import _strtobool from _pytest.config import Config from _pytest.config import create_terminal_writer from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.pathlib import Path from _pytest.store import StoreKey +from _pytest.terminal import TerminalReporter DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" @@ -32,7 +38,7 @@ catch_log_records_key = StoreKey[Dict[str, List[logging.LogRecord]]]() -def _remove_ansi_escape_sequences(text): +def _remove_ansi_escape_sequences(text: str) -> str: return _ANSI_ESCAPE_SEQ.sub("", text) @@ -52,7 +58,7 @@ class ColoredLevelFormatter(logging.Formatter): } # type: Mapping[int, AbstractSet[str]] LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*s)") - def __init__(self, terminalwriter, *args, **kwargs) -> None: + def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._original_fmt = self._style._fmt self._level_to_fmt_mapping = {} # type: Dict[int, str] @@ -77,7 +83,7 @@ def __init__(self, terminalwriter, *args, **kwargs) -> None: colorized_formatted_levelname, self._fmt ) - def format(self, record): + def format(self, record: logging.LogRecord) -> str: fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt) self._style._fmt = fmt return super().format(record) @@ -90,18 +96,20 @@ class PercentStyleMultiline(logging.PercentStyle): formats the message as if each line were logged separately. """ - def __init__(self, fmt, auto_indent): + def __init__(self, fmt: str, auto_indent: Union[int, str, bool]) -> None: super().__init__(fmt) self._auto_indent = self._get_auto_indent(auto_indent) @staticmethod - def _update_message(record_dict, message): + def _update_message( + record_dict: Dict[str, object], message: str + ) -> Dict[str, object]: tmp = record_dict.copy() tmp["message"] = message return tmp @staticmethod - def _get_auto_indent(auto_indent_option) -> int: + def _get_auto_indent(auto_indent_option: Union[int, str, bool]) -> int: """Determines the current auto indentation setting Specify auto indent behavior (on/off/fixed) by passing in @@ -149,11 +157,11 @@ def _get_auto_indent(auto_indent_option) -> int: return 0 - def format(self, record): + def format(self, record: logging.LogRecord) -> str: if "\n" in record.message: if hasattr(record, "auto_indent"): # passed in from the "extra={}" kwarg on the call to logging.log() - auto_indent = self._get_auto_indent(record.auto_indent) + auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined] # noqa: F821 else: auto_indent = self._auto_indent @@ -173,7 +181,7 @@ def format(self, record): return self._fmt % record.__dict__ -def get_option_ini(config, *names): +def get_option_ini(config: Config, *names: str): for name in names: ret = config.getoption(name) # 'default' arg won't work as expected if ret is None: @@ -268,13 +276,16 @@ def add_option_ini(option, dest, default=None, type=None, **kwargs): ) +_HandlerType = TypeVar("_HandlerType", bound=logging.Handler) + + # Not using @contextmanager for performance reasons. class catching_logs: """Context manager that prepares the whole logging machinery properly.""" __slots__ = ("handler", "level", "orig_level") - def __init__(self, handler, level=None): + def __init__(self, handler: _HandlerType, level: Optional[int] = None) -> None: self.handler = handler self.level = level @@ -330,7 +341,7 @@ def __init__(self, item: nodes.Node) -> None: """Creates a new funcarg.""" self._item = item # dict of log name -> log level - self._initial_log_levels = {} # type: Dict[str, int] + self._initial_log_levels = {} # type: Dict[Optional[str], int] def _finalize(self) -> None: """Finalizes the fixture. @@ -364,17 +375,17 @@ def get_records(self, when: str) -> List[logging.LogRecord]: return self._item._store[catch_log_records_key].get(when, []) @property - def text(self): + def text(self) -> str: """Returns the formatted log text.""" return _remove_ansi_escape_sequences(self.handler.stream.getvalue()) @property - def records(self): + def records(self) -> List[logging.LogRecord]: """Returns the list of log records.""" return self.handler.records @property - def record_tuples(self): + def record_tuples(self) -> List[Tuple[str, int, str]]: """Returns a list of a stripped down version of log records intended for use in assertion comparison. @@ -385,7 +396,7 @@ def record_tuples(self): return [(r.name, r.levelno, r.getMessage()) for r in self.records] @property - def messages(self): + def messages(self) -> List[str]: """Returns a list of format-interpolated log messages. Unlike 'records', which contains the format string and parameters for interpolation, log messages in this list @@ -400,11 +411,11 @@ def messages(self): """ return [r.getMessage() for r in self.records] - def clear(self): + def clear(self) -> None: """Reset the list of log records and the captured log text.""" self.handler.reset() - def set_level(self, level, logger=None): + def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None: """Sets the level for capturing of logs. The level will be restored to its previous value at the end of the test. @@ -415,31 +426,32 @@ def set_level(self, level, logger=None): The levels of the loggers changed by this function will be restored to their initial values at the end of the test. """ - logger_name = logger - logger = logging.getLogger(logger_name) + logger_obj = logging.getLogger(logger) # save the original log-level to restore it during teardown - self._initial_log_levels.setdefault(logger_name, logger.level) - logger.setLevel(level) + self._initial_log_levels.setdefault(logger, logger_obj.level) + logger_obj.setLevel(level) @contextmanager - def at_level(self, level, logger=None): + def at_level( + self, level: int, logger: Optional[str] = None + ) -> Generator[None, None, None]: """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the level is restored to its original value. :param int level: the logger to level. :param str logger: the logger to update the level. If not given, the root logger level is updated. """ - logger = logging.getLogger(logger) - orig_level = logger.level - logger.setLevel(level) + logger_obj = logging.getLogger(logger) + orig_level = logger_obj.level + logger_obj.setLevel(level) try: yield finally: - logger.setLevel(orig_level) + logger_obj.setLevel(orig_level) @pytest.fixture -def caplog(request): +def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]: """Access and control log capturing. Captured logs are available through the following properties/methods:: @@ -557,7 +569,7 @@ def _create_formatter(self, log_format, log_date_format, auto_indent): return formatter - def set_log_path(self, fname): + def set_log_path(self, fname: str) -> None: """Public method, which can set filename parameter for Logging.FileHandler(). Also creates parent directory if it does not exist. @@ -565,15 +577,15 @@ def set_log_path(self, fname): .. warning:: Please considered as an experimental API. """ - fname = Path(fname) + fpath = Path(fname) - if not fname.is_absolute(): - fname = Path(self._config.rootdir, fname) + if not fpath.is_absolute(): + fpath = Path(self._config.rootdir, fpath) - if not fname.parent.exists(): - fname.parent.mkdir(exist_ok=True, parents=True) + if not fpath.parent.exists(): + fpath.parent.mkdir(exist_ok=True, parents=True) - stream = fname.open(mode="w", encoding="UTF-8") + stream = fpath.open(mode="w", encoding="UTF-8") if sys.version_info >= (3, 7): old_stream = self.log_file_handler.setStream(stream) else: @@ -715,29 +727,35 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): and won't appear in the terminal. """ - def __init__(self, terminal_reporter, capture_manager): + # Officially stream needs to be a IO[str], but TerminalReporter + # isn't. So force it. + stream = None # type: TerminalReporter # type: ignore + + def __init__( + self, terminal_reporter: TerminalReporter, capture_manager: CaptureManager + ) -> None: """ :param _pytest.terminal.TerminalReporter terminal_reporter: :param _pytest.capture.CaptureManager capture_manager: """ - logging.StreamHandler.__init__(self, stream=terminal_reporter) + logging.StreamHandler.__init__(self, stream=terminal_reporter) # type: ignore[arg-type] # noqa: F821 self.capture_manager = capture_manager self.reset() self.set_when(None) self._test_outcome_written = False - def reset(self): + def reset(self) -> None: """Reset the handler; should be called before the start of each test""" self._first_record_emitted = False - def set_when(self, when): + def set_when(self, when: Optional[str]) -> None: """Prepares for the given test phase (setup/call/teardown)""" self._when = when self._section_name_shown = False if when == "start": self._test_outcome_written = False - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: ctx_manager = ( self.capture_manager.global_and_fixture_disabled() if self.capture_manager @@ -764,10 +782,10 @@ def handleError(self, record: logging.LogRecord) -> None: class _LiveLoggingNullHandler(logging.NullHandler): """A handler used when live logging is disabled.""" - def reset(self): + def reset(self) -> None: pass - def set_when(self, when): + def set_when(self, when: str) -> None: pass def handleError(self, record: logging.LogRecord) -> None: From b51ea4f1a579b3ff3a8c122bc6218c7c109d8ecd Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 313/823] Type annotate _pytest.unittest --- src/_pytest/unittest.py | 86 ++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index f9eb6e71939..a90b56c2962 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -1,17 +1,23 @@ """ discovery and running of std-library "unittest" style tests. """ import sys import traceback +import types from typing import Any +from typing import Callable from typing import Generator from typing import Iterable +from typing import List from typing import Optional +from typing import Tuple from typing import Union import _pytest._code import pytest from _pytest.compat import getimfunc from _pytest.compat import is_async_function +from _pytest.compat import TYPE_CHECKING from _pytest.config import hookimpl +from _pytest.fixtures import FixtureRequest from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.outcomes import exit @@ -25,6 +31,17 @@ from _pytest.skipping import skipped_by_mark_key from _pytest.skipping import unexpectedsuccess_key +if TYPE_CHECKING: + import unittest + from typing import Type + + from _pytest.fixtures import _Scope + + _SysExcInfoType = Union[ + Tuple[Type[BaseException], BaseException, types.TracebackType], + Tuple[None, None, None], + ] + def pytest_pycollect_makeitem( collector: PyCollector, name: str, obj @@ -78,30 +95,32 @@ def collect(self) -> Iterable[Union[Item, Collector]]: if ut is None or runtest != ut.TestCase.runTest: # type: ignore yield TestCaseFunction.from_parent(self, name="runTest") - def _inject_setup_teardown_fixtures(self, cls): + def _inject_setup_teardown_fixtures(self, cls: type) -> None: """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding teardown functions (#517)""" class_fixture = _make_xunit_fixture( cls, "setUpClass", "tearDownClass", scope="class", pass_self=False ) if class_fixture: - cls.__pytest_class_setup = class_fixture + cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined] # noqa: F821 method_fixture = _make_xunit_fixture( cls, "setup_method", "teardown_method", scope="function", pass_self=True ) if method_fixture: - cls.__pytest_method_setup = method_fixture + cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined] # noqa: F821 -def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self): +def _make_xunit_fixture( + obj: type, setup_name: str, teardown_name: str, scope: "_Scope", pass_self: bool +): setup = getattr(obj, setup_name, None) teardown = getattr(obj, teardown_name, None) if setup is None and teardown is None: return None @pytest.fixture(scope=scope, autouse=True) - def fixture(self, request): + def fixture(self, request: FixtureRequest) -> Generator[None, None, None]: if _is_skipped(self): reason = self.__unittest_skip_why__ pytest.skip(reason) @@ -122,32 +141,33 @@ def fixture(self, request): class TestCaseFunction(Function): nofuncargs = True - _excinfo = None - _testcase = None + _excinfo = None # type: Optional[List[_pytest._code.ExceptionInfo]] + _testcase = None # type: Optional[unittest.TestCase] - def setup(self): + def setup(self) -> None: # a bound method to be called during teardown() if set (see 'runtest()') - self._explicit_tearDown = None - self._testcase = self.parent.obj(self.name) + self._explicit_tearDown = None # type: Optional[Callable[[], None]] + assert self.parent is not None + self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined] # noqa: F821 self._obj = getattr(self._testcase, self.name) if hasattr(self, "_request"): self._request._fillfixtures() - def teardown(self): + def teardown(self) -> None: if self._explicit_tearDown is not None: self._explicit_tearDown() self._explicit_tearDown = None self._testcase = None self._obj = None - def startTest(self, testcase): + def startTest(self, testcase: "unittest.TestCase") -> None: pass - def _addexcinfo(self, rawexcinfo): + def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None: # unwrap potential exception info (see twisted trial support below) rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) try: - excinfo = _pytest._code.ExceptionInfo(rawexcinfo) + excinfo = _pytest._code.ExceptionInfo(rawexcinfo) # type: ignore[arg-type] # noqa: F821 # invoke the attributes to trigger storing the traceback # trial causes some issue there excinfo.value @@ -176,7 +196,9 @@ def _addexcinfo(self, rawexcinfo): excinfo = _pytest._code.ExceptionInfo.from_current() self.__dict__.setdefault("_excinfo", []).append(excinfo) - def addError(self, testcase, rawexcinfo): + def addError( + self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType" + ) -> None: try: if isinstance(rawexcinfo[1], exit.Exception): exit(rawexcinfo[1].msg) @@ -184,29 +206,38 @@ def addError(self, testcase, rawexcinfo): pass self._addexcinfo(rawexcinfo) - def addFailure(self, testcase, rawexcinfo): + def addFailure( + self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType" + ) -> None: self._addexcinfo(rawexcinfo) - def addSkip(self, testcase, reason): + def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None: try: skip(reason) except skip.Exception: self._store[skipped_by_mark_key] = True self._addexcinfo(sys.exc_info()) - def addExpectedFailure(self, testcase, rawexcinfo, reason=""): + def addExpectedFailure( + self, + testcase: "unittest.TestCase", + rawexcinfo: "_SysExcInfoType", + reason: str = "", + ) -> None: try: xfail(str(reason)) except xfail.Exception: self._addexcinfo(sys.exc_info()) - def addUnexpectedSuccess(self, testcase, reason=""): + def addUnexpectedSuccess( + self, testcase: "unittest.TestCase", reason: str = "" + ) -> None: self._store[unexpectedsuccess_key] = reason - def addSuccess(self, testcase): + def addSuccess(self, testcase: "unittest.TestCase") -> None: pass - def stopTest(self, testcase): + def stopTest(self, testcase: "unittest.TestCase") -> None: pass def _expecting_failure(self, test_method) -> bool: @@ -218,14 +249,17 @@ def _expecting_failure(self, test_method) -> bool: expecting_failure_class = getattr(self, "__unittest_expecting_failure__", False) return bool(expecting_failure_class or expecting_failure_method) - def runtest(self): + def runtest(self) -> None: from _pytest.debugging import maybe_wrap_pytest_function_for_tracing + assert self._testcase is not None + maybe_wrap_pytest_function_for_tracing(self) # let the unittest framework handle async functions if is_async_function(self.obj): - self._testcase(self) + # Type ignored because self acts as the TestResult, but is not actually one. + self._testcase(result=self) # type: ignore[arg-type] # noqa: F821 else: # when --pdb is given, we want to postpone calling tearDown() otherwise # when entering the pdb prompt, tearDown() would have probably cleaned up @@ -241,11 +275,11 @@ def runtest(self): # wrap_pytest_function_for_tracing replaces self.obj by a wrapper setattr(self._testcase, self.name, self.obj) try: - self._testcase(result=self) + self._testcase(result=self) # type: ignore[arg-type] # noqa: F821 finally: delattr(self._testcase, self.name) - def _prunetraceback(self, excinfo): + def _prunetraceback(self, excinfo: _pytest._code.ExceptionInfo) -> None: Function._prunetraceback(self, excinfo) traceback = excinfo.traceback.filter( lambda x: not x.frame.f_globals.get("__unittest") @@ -313,7 +347,7 @@ def excstore( yield -def check_testcase_implements_trial_reporter(done=[]): +def check_testcase_implements_trial_reporter(done: List[int] = []) -> None: if done: return from zope.interface import classImplements From 3e351afeb3443bb6c4940d80d8517693df71b397 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 314/823] Type annotate _pytest.capture --- src/_pytest/capture.py | 112 ++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 13931ca103c..bcc16ceb6af 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -9,9 +9,11 @@ import sys from io import UnsupportedOperation from tempfile import TemporaryFile +from typing import Generator from typing import Optional from typing import TextIO from typing import Tuple +from typing import Union import pytest from _pytest.compat import TYPE_CHECKING @@ -46,7 +48,7 @@ def pytest_addoption(parser: Parser) -> None: ) -def _colorama_workaround(): +def _colorama_workaround() -> None: """ Ensure colorama is imported so that it attaches to the correct stdio handles on Windows. @@ -62,7 +64,7 @@ def _colorama_workaround(): pass -def _readline_workaround(): +def _readline_workaround() -> None: """ Ensure readline is imported so that it attaches to the correct stdio handles on Windows. @@ -87,7 +89,7 @@ def _readline_workaround(): pass -def _py36_windowsconsoleio_workaround(stream): +def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: """ Python 3.6 implemented unicode console handling for Windows. This works by reading/writing to the raw console handle using @@ -202,7 +204,7 @@ def __init__(self, other: TextIO) -> None: self._other = other super().__init__() - def write(self, s) -> int: + def write(self, s: str) -> int: super().write(s) return self._other.write(s) @@ -222,13 +224,13 @@ def read(self, *args): def __iter__(self): return self - def fileno(self): + def fileno(self) -> int: raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()") - def isatty(self): + def isatty(self) -> bool: return False - def close(self): + def close(self) -> None: pass @property @@ -251,7 +253,7 @@ class SysCaptureBinary: EMPTY_BUFFER = b"" - def __init__(self, fd, tmpfile=None, *, tee=False): + def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None: name = patchsysdict[fd] self._old = getattr(sys, name) self.name = name @@ -288,7 +290,7 @@ def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: op, self._state, ", ".join(states) ) - def start(self): + def start(self) -> None: self._assert_state("start", ("initialized",)) setattr(sys, self.name, self.tmpfile) self._state = "started" @@ -301,7 +303,7 @@ def snap(self): self.tmpfile.truncate() return res - def done(self): + def done(self) -> None: self._assert_state("done", ("initialized", "started", "suspended", "done")) if self._state == "done": return @@ -310,19 +312,19 @@ def done(self): self.tmpfile.close() self._state = "done" - def suspend(self): + def suspend(self) -> None: self._assert_state("suspend", ("started", "suspended")) setattr(sys, self.name, self._old) self._state = "suspended" - def resume(self): + def resume(self) -> None: self._assert_state("resume", ("started", "suspended")) if self._state == "started": return setattr(sys, self.name, self.tmpfile) self._state = "started" - def writeorg(self, data): + def writeorg(self, data) -> None: self._assert_state("writeorg", ("started", "suspended")) self._old.flush() self._old.buffer.write(data) @@ -352,7 +354,7 @@ class FDCaptureBinary: EMPTY_BUFFER = b"" - def __init__(self, targetfd): + def __init__(self, targetfd: int) -> None: self.targetfd = targetfd try: @@ -369,7 +371,9 @@ def __init__(self, targetfd): # Further complications are the need to support suspend() and the # possibility of FD reuse (e.g. the tmpfile getting the very same # target FD). The following approach is robust, I believe. - self.targetfd_invalid = os.open(os.devnull, os.O_RDWR) + self.targetfd_invalid = os.open( + os.devnull, os.O_RDWR + ) # type: Optional[int] os.dup2(self.targetfd_invalid, targetfd) else: self.targetfd_invalid = None @@ -380,7 +384,8 @@ def __init__(self, targetfd): self.syscapture = SysCapture(targetfd) else: self.tmpfile = EncodedFile( - TemporaryFile(buffering=0), + # TODO: Remove type ignore, fixed in next mypy release. + TemporaryFile(buffering=0), # type: ignore[arg-type] encoding="utf-8", errors="replace", write_through=True, @@ -392,7 +397,7 @@ def __init__(self, targetfd): self._state = "initialized" - def __repr__(self): + def __repr__(self) -> str: return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format( self.__class__.__name__, self.targetfd, @@ -408,7 +413,7 @@ def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: op, self._state, ", ".join(states) ) - def start(self): + def start(self) -> None: """ Start capturing on targetfd using memorized tmpfile. """ self._assert_state("start", ("initialized",)) os.dup2(self.tmpfile.fileno(), self.targetfd) @@ -423,7 +428,7 @@ def snap(self): self.tmpfile.truncate() return res - def done(self): + def done(self) -> None: """ stop capturing, restore streams, return original capture file, seeked to position zero. """ self._assert_state("done", ("initialized", "started", "suspended", "done")) @@ -439,7 +444,7 @@ def done(self): self.tmpfile.close() self._state = "done" - def suspend(self): + def suspend(self) -> None: self._assert_state("suspend", ("started", "suspended")) if self._state == "suspended": return @@ -447,7 +452,7 @@ def suspend(self): os.dup2(self.targetfd_save, self.targetfd) self._state = "suspended" - def resume(self): + def resume(self) -> None: self._assert_state("resume", ("started", "suspended")) if self._state == "started": return @@ -497,12 +502,12 @@ def __init__(self, in_, out, err) -> None: self.out = out self.err = err - def __repr__(self): + def __repr__(self) -> str: return "".format( self.out, self.err, self.in_, self._state, self._in_suspended, ) - def start_capturing(self): + def start_capturing(self) -> None: self._state = "started" if self.in_: self.in_.start() @@ -520,7 +525,7 @@ def pop_outerr_to_orig(self): self.err.writeorg(err) return out, err - def suspend_capturing(self, in_=False): + def suspend_capturing(self, in_: bool = False) -> None: self._state = "suspended" if self.out: self.out.suspend() @@ -530,7 +535,7 @@ def suspend_capturing(self, in_=False): self.in_.suspend() self._in_suspended = True - def resume_capturing(self): + def resume_capturing(self) -> None: self._state = "resumed" if self.out: self.out.resume() @@ -540,7 +545,7 @@ def resume_capturing(self): self.in_.resume() self._in_suspended = False - def stop_capturing(self): + def stop_capturing(self) -> None: """ stop capturing and reset capturing streams """ if self._state == "stopped": raise ValueError("was already stopped") @@ -596,15 +601,15 @@ class CaptureManager: def __init__(self, method: "_CaptureMethod") -> None: self._method = method - self._global_capturing = None + self._global_capturing = None # type: Optional[MultiCapture] self._capture_fixture = None # type: Optional[CaptureFixture] - def __repr__(self): + def __repr__(self) -> str: return "".format( self._method, self._global_capturing, self._capture_fixture ) - def is_capturing(self): + def is_capturing(self) -> Union[str, bool]: if self.is_globally_capturing(): return "global" if self._capture_fixture: @@ -613,40 +618,41 @@ def is_capturing(self): # Global capturing control - def is_globally_capturing(self): + def is_globally_capturing(self) -> bool: return self._method != "no" - def start_global_capturing(self): + def start_global_capturing(self) -> None: assert self._global_capturing is None self._global_capturing = _get_multicapture(self._method) self._global_capturing.start_capturing() - def stop_global_capturing(self): + def stop_global_capturing(self) -> None: if self._global_capturing is not None: self._global_capturing.pop_outerr_to_orig() self._global_capturing.stop_capturing() self._global_capturing = None - def resume_global_capture(self): + def resume_global_capture(self) -> None: # During teardown of the python process, and on rare occasions, capture # attributes can be `None` while trying to resume global capture. if self._global_capturing is not None: self._global_capturing.resume_capturing() - def suspend_global_capture(self, in_=False): + def suspend_global_capture(self, in_: bool = False) -> None: if self._global_capturing is not None: self._global_capturing.suspend_capturing(in_=in_) - def suspend(self, in_=False): + def suspend(self, in_: bool = False) -> None: # Need to undo local capsys-et-al if it exists before disabling global capture. self.suspend_fixture() self.suspend_global_capture(in_) - def resume(self): + def resume(self) -> None: self.resume_global_capture() self.resume_fixture() def read_global_capture(self): + assert self._global_capturing is not None return self._global_capturing.readouterr() # Fixture Control @@ -665,30 +671,30 @@ def set_fixture(self, capture_fixture: "CaptureFixture") -> None: def unset_fixture(self) -> None: self._capture_fixture = None - def activate_fixture(self): + def activate_fixture(self) -> None: """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over the global capture. """ if self._capture_fixture: self._capture_fixture._start() - def deactivate_fixture(self): + def deactivate_fixture(self) -> None: """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any.""" if self._capture_fixture: self._capture_fixture.close() - def suspend_fixture(self): + def suspend_fixture(self) -> None: if self._capture_fixture: self._capture_fixture._suspend() - def resume_fixture(self): + def resume_fixture(self) -> None: if self._capture_fixture: self._capture_fixture._resume() # Helper context managers @contextlib.contextmanager - def global_and_fixture_disabled(self): + def global_and_fixture_disabled(self) -> Generator[None, None, None]: """Context manager to temporarily disable global and current fixture capturing.""" self.suspend() try: @@ -697,7 +703,7 @@ def global_and_fixture_disabled(self): self.resume() @contextlib.contextmanager - def item_capture(self, when, item): + def item_capture(self, when: str, item: Item) -> Generator[None, None, None]: self.resume_global_capture() self.activate_fixture() try: @@ -757,21 +763,21 @@ class CaptureFixture: fixtures. """ - def __init__(self, captureclass, request): + def __init__(self, captureclass, request: SubRequest) -> None: self.captureclass = captureclass self.request = request - self._capture = None + self._capture = None # type: Optional[MultiCapture] self._captured_out = self.captureclass.EMPTY_BUFFER self._captured_err = self.captureclass.EMPTY_BUFFER - def _start(self): + def _start(self) -> None: if self._capture is None: self._capture = MultiCapture( in_=None, out=self.captureclass(1), err=self.captureclass(2), ) self._capture.start_capturing() - def close(self): + def close(self) -> None: if self._capture is not None: out, err = self._capture.pop_outerr_to_orig() self._captured_out += out @@ -793,18 +799,18 @@ def readouterr(self): self._captured_err = self.captureclass.EMPTY_BUFFER return CaptureResult(captured_out, captured_err) - def _suspend(self): + def _suspend(self) -> None: """Suspends this fixture's own capturing temporarily.""" if self._capture is not None: self._capture.suspend_capturing() - def _resume(self): + def _resume(self) -> None: """Resumes this fixture's own capturing temporarily.""" if self._capture is not None: self._capture.resume_capturing() @contextlib.contextmanager - def disabled(self): + def disabled(self) -> Generator[None, None, None]: """Temporarily disables capture while inside the 'with' block.""" capmanager = self.request.config.pluginmanager.getplugin("capturemanager") with capmanager.global_and_fixture_disabled(): @@ -815,7 +821,7 @@ def disabled(self): @pytest.fixture -def capsys(request): +def capsys(request: SubRequest): """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method @@ -832,7 +838,7 @@ def capsys(request): @pytest.fixture -def capsysbinary(request): +def capsysbinary(request: SubRequest): """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` @@ -849,7 +855,7 @@ def capsysbinary(request): @pytest.fixture -def capfd(request): +def capfd(request: SubRequest): """Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -866,7 +872,7 @@ def capfd(request): @pytest.fixture -def capfdbinary(request): +def capfdbinary(request: SubRequest): """Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method From 216a010ab70ca88f8d68f051c2b732fc90380e70 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 315/823] Type annotate _pytest.junitxml --- src/_pytest/junitxml.py | 147 +++++++++++++++++++++++----------------- src/_pytest/nodes.py | 3 +- 2 files changed, 84 insertions(+), 66 deletions(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 0ecfb09bb54..47ba89d38cd 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -14,6 +14,11 @@ import re import sys from datetime import datetime +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple +from typing import Union import py @@ -21,14 +26,19 @@ from _pytest import deprecated from _pytest import nodes from _pytest import timing +from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import filename_arg from _pytest.config.argparsing import Parser +from _pytest.fixtures import FixtureRequest from _pytest.reports import TestReport from _pytest.store import StoreKey from _pytest.terminal import TerminalReporter from _pytest.warnings import _issue_warning_captured +if TYPE_CHECKING: + from typing import Type + xml_key = StoreKey["LogXML"]() @@ -58,8 +68,8 @@ class Junit(py.xml.Namespace): _py_ext_re = re.compile(r"\.py$") -def bin_xml_escape(arg): - def repl(matchobj): +def bin_xml_escape(arg: str) -> py.xml.raw: + def repl(matchobj: "re.Match[str]") -> str: i = ord(matchobj.group()) if i <= 0xFF: return "#x%02X" % i @@ -69,7 +79,7 @@ def repl(matchobj): return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg))) -def merge_family(left, right): +def merge_family(left, right) -> None: result = {} for kl, vl in left.items(): for kr, vr in right.items(): @@ -92,28 +102,27 @@ def merge_family(left, right): class _NodeReporter: - def __init__(self, nodeid, xml): + def __init__(self, nodeid: Union[str, TestReport], xml: "LogXML") -> None: self.id = nodeid self.xml = xml self.add_stats = self.xml.add_stats self.family = self.xml.family self.duration = 0 - self.properties = [] - self.nodes = [] - self.testcase = None - self.attrs = {} + self.properties = [] # type: List[Tuple[str, py.xml.raw]] + self.nodes = [] # type: List[py.xml.Tag] + self.attrs = {} # type: Dict[str, Union[str, py.xml.raw]] - def append(self, node): + def append(self, node: py.xml.Tag) -> None: self.xml.add_stats(type(node).__name__) self.nodes.append(node) - def add_property(self, name, value): + def add_property(self, name: str, value: str) -> None: self.properties.append((str(name), bin_xml_escape(value))) - def add_attribute(self, name, value): + def add_attribute(self, name: str, value: str) -> None: self.attrs[str(name)] = bin_xml_escape(value) - def make_properties_node(self): + def make_properties_node(self) -> Union[py.xml.Tag, str]: """Return a Junit node containing custom properties, if any. """ if self.properties: @@ -125,8 +134,7 @@ def make_properties_node(self): ) return "" - def record_testreport(self, testreport): - assert not self.testcase + def record_testreport(self, testreport: TestReport) -> None: names = mangle_test_address(testreport.nodeid) existing_attrs = self.attrs classnames = names[:-1] @@ -136,9 +144,9 @@ def record_testreport(self, testreport): "classname": ".".join(classnames), "name": bin_xml_escape(names[-1]), "file": testreport.location[0], - } + } # type: Dict[str, Union[str, py.xml.raw]] if testreport.location[1] is not None: - attrs["line"] = testreport.location[1] + attrs["line"] = str(testreport.location[1]) if hasattr(testreport, "url"): attrs["url"] = testreport.url self.attrs = attrs @@ -156,19 +164,19 @@ def record_testreport(self, testreport): temp_attrs[key] = self.attrs[key] self.attrs = temp_attrs - def to_xml(self): + def to_xml(self) -> py.xml.Tag: testcase = Junit.testcase(time="%.3f" % self.duration, **self.attrs) testcase.append(self.make_properties_node()) for node in self.nodes: testcase.append(node) return testcase - def _add_simple(self, kind, message, data=None): + def _add_simple(self, kind: "Type[py.xml.Tag]", message: str, data=None) -> None: data = bin_xml_escape(data) node = kind(data, message=message) self.append(node) - def write_captured_output(self, report): + def write_captured_output(self, report: TestReport) -> None: if not self.xml.log_passing_tests and report.passed: return @@ -191,21 +199,22 @@ def write_captured_output(self, report): if content_all: self._write_content(report, content_all, "system-out") - def _prepare_content(self, content, header): + def _prepare_content(self, content: str, header: str) -> str: return "\n".join([header.center(80, "-"), content, ""]) - def _write_content(self, report, content, jheader): + def _write_content(self, report: TestReport, content: str, jheader: str) -> None: tag = getattr(Junit, jheader) self.append(tag(bin_xml_escape(content))) - def append_pass(self, report): + def append_pass(self, report: TestReport) -> None: self.add_stats("passed") - def append_failure(self, report): + def append_failure(self, report: TestReport) -> None: # msg = str(report.longrepr.reprtraceback.extraline) if hasattr(report, "wasxfail"): self._add_simple(Junit.skipped, "xfail-marked test passes unexpectedly") else: + assert report.longrepr is not None if getattr(report.longrepr, "reprcrash", None) is not None: message = report.longrepr.reprcrash.message else: @@ -215,23 +224,24 @@ def append_failure(self, report): fail.append(bin_xml_escape(report.longrepr)) self.append(fail) - def append_collect_error(self, report): + def append_collect_error(self, report: TestReport) -> None: # msg = str(report.longrepr.reprtraceback.extraline) + assert report.longrepr is not None self.append( Junit.error(bin_xml_escape(report.longrepr), message="collection failure") ) - def append_collect_skipped(self, report): + def append_collect_skipped(self, report: TestReport) -> None: self._add_simple(Junit.skipped, "collection skipped", report.longrepr) - def append_error(self, report): + def append_error(self, report: TestReport) -> None: if report.when == "teardown": msg = "test teardown failure" else: msg = "test setup failure" self._add_simple(Junit.error, msg, report.longrepr) - def append_skipped(self, report): + def append_skipped(self, report: TestReport) -> None: if hasattr(report, "wasxfail"): xfailreason = report.wasxfail if xfailreason.startswith("reason: "): @@ -242,6 +252,7 @@ def append_skipped(self, report): ) ) else: + assert report.longrepr is not None filename, lineno, skipreason = report.longrepr if skipreason.startswith("Skipped: "): skipreason = skipreason[9:] @@ -256,13 +267,17 @@ def append_skipped(self, report): ) self.write_captured_output(report) - def finalize(self): + def finalize(self) -> None: data = self.to_xml().unicode(indent=0) self.__dict__.clear() - self.to_xml = lambda: py.xml.raw(data) + # Type ignored becuase mypy doesn't like overriding a method. + # Also the return value doesn't match... + self.to_xml = lambda: py.xml.raw(data) # type: ignore # noqa: F821 -def _warn_incompatibility_with_xunit2(request, fixture_name): +def _warn_incompatibility_with_xunit2( + request: FixtureRequest, fixture_name: str +) -> None: """Emits a PytestWarning about the given fixture being incompatible with newer xunit revisions""" from _pytest.warning_types import PytestWarning @@ -278,7 +293,7 @@ def _warn_incompatibility_with_xunit2(request, fixture_name): @pytest.fixture -def record_property(request): +def record_property(request: FixtureRequest): """Add an extra properties the calling test. User properties become part of the test report and are available to the configured reporters, like JUnit XML. @@ -292,14 +307,14 @@ def test_function(record_property): """ _warn_incompatibility_with_xunit2(request, "record_property") - def append_property(name, value): + def append_property(name: str, value: object) -> None: request.node.user_properties.append((name, value)) return append_property @pytest.fixture -def record_xml_attribute(request): +def record_xml_attribute(request: FixtureRequest): """Add extra xml attributes to the tag for the calling test. The fixture is callable with ``(name, value)``, with value being automatically xml-encoded @@ -313,7 +328,7 @@ def record_xml_attribute(request): _warn_incompatibility_with_xunit2(request, "record_xml_attribute") # Declare noop - def add_attr_noop(name, value): + def add_attr_noop(name: str, value: str) -> None: pass attr_func = add_attr_noop @@ -326,7 +341,7 @@ def add_attr_noop(name, value): return attr_func -def _check_record_param_type(param, v): +def _check_record_param_type(param: str, v: str) -> None: """Used by record_testsuite_property to check that the given parameter name is of the proper type""" __tracebackhide__ = True @@ -336,7 +351,7 @@ def _check_record_param_type(param, v): @pytest.fixture(scope="session") -def record_testsuite_property(request): +def record_testsuite_property(request: FixtureRequest): """ Records a new ```` tag as child of the root ````. This is suitable to writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family. @@ -354,7 +369,7 @@ def test_foo(record_testsuite_property): __tracebackhide__ = True - def record_func(name, value): + def record_func(name: str, value: str): """noop function in case --junitxml was not passed in the command-line""" __tracebackhide__ = True _check_record_param_type("name", name) @@ -437,7 +452,7 @@ def pytest_unconfigure(config: Config) -> None: config.pluginmanager.unregister(xml) -def mangle_test_address(address): +def mangle_test_address(address: str) -> List[str]: path, possible_open_bracket, params = address.partition("[") names = path.split("::") try: @@ -456,13 +471,13 @@ class LogXML: def __init__( self, logfile, - prefix, - suite_name="pytest", - logging="no", - report_duration="total", + prefix: Optional[str], + suite_name: str = "pytest", + logging: str = "no", + report_duration: str = "total", family="xunit1", - log_passing_tests=True, - ): + log_passing_tests: bool = True, + ) -> None: logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) self.prefix = prefix @@ -471,20 +486,24 @@ def __init__( self.log_passing_tests = log_passing_tests self.report_duration = report_duration self.family = family - self.stats = dict.fromkeys(["error", "passed", "failure", "skipped"], 0) - self.node_reporters = {} # nodeid -> _NodeReporter - self.node_reporters_ordered = [] - self.global_properties = [] + self.stats = dict.fromkeys( + ["error", "passed", "failure", "skipped"], 0 + ) # type: Dict[str, int] + self.node_reporters = ( + {} + ) # type: Dict[Tuple[Union[str, TestReport], object], _NodeReporter] + self.node_reporters_ordered = [] # type: List[_NodeReporter] + self.global_properties = [] # type: List[Tuple[str, py.xml.raw]] # List of reports that failed on call but teardown is pending. - self.open_reports = [] + self.open_reports = [] # type: List[TestReport] self.cnt_double_fail_tests = 0 # Replaces convenience family with real family if self.family == "legacy": self.family = "xunit1" - def finalize(self, report): + def finalize(self, report: TestReport) -> None: nodeid = getattr(report, "nodeid", report) # local hack to handle xdist report order slavenode = getattr(report, "node", None) @@ -492,8 +511,8 @@ def finalize(self, report): if reporter is not None: reporter.finalize() - def node_reporter(self, report): - nodeid = getattr(report, "nodeid", report) + def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter: + nodeid = getattr(report, "nodeid", report) # type: Union[str, TestReport] # local hack to handle xdist report order slavenode = getattr(report, "node", None) @@ -510,11 +529,11 @@ def node_reporter(self, report): return reporter - def add_stats(self, key): + def add_stats(self, key: str) -> None: if key in self.stats: self.stats[key] += 1 - def _opentestcase(self, report): + def _opentestcase(self, report: TestReport) -> _NodeReporter: reporter = self.node_reporter(report) reporter.record_testreport(report) return reporter @@ -587,7 +606,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: reporter.write_captured_output(report) for propname, propvalue in report.user_properties: - reporter.add_property(propname, propvalue) + reporter.add_property(propname, str(propvalue)) self.finalize(report) report_wid = getattr(report, "worker_id", None) @@ -607,7 +626,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: if close_report: self.open_reports.remove(close_report) - def update_testcase_duration(self, report): + def update_testcase_duration(self, report: TestReport) -> None: """accumulates total duration for nodeid from given report and updates the Junit.testcase with the new total if already created. """ @@ -615,7 +634,7 @@ def update_testcase_duration(self, report): reporter = self.node_reporter(report) reporter.duration += getattr(report, "duration", 0.0) - def pytest_collectreport(self, report): + def pytest_collectreport(self, report: TestReport) -> None: if not report.passed: reporter = self._opentestcase(report) if report.failed: @@ -623,7 +642,7 @@ def pytest_collectreport(self, report): else: reporter.append_collect_skipped(report) - def pytest_internalerror(self, excrepr): + def pytest_internalerror(self, excrepr) -> None: reporter = self.node_reporter("internal") reporter.attrs.update(classname="pytest", name="internal") reporter._add_simple(Junit.error, "internal error", excrepr) @@ -652,10 +671,10 @@ def pytest_sessionfinish(self) -> None: self._get_global_properties_node(), [x.to_xml() for x in self.node_reporters_ordered], name=self.suite_name, - errors=self.stats["error"], - failures=self.stats["failure"], - skipped=self.stats["skipped"], - tests=numtests, + errors=str(self.stats["error"]), + failures=str(self.stats["failure"]), + skipped=str(self.stats["skipped"]), + tests=str(numtests), time="%.3f" % suite_time_delta, timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(), hostname=platform.node(), @@ -666,12 +685,12 @@ def pytest_sessionfinish(self) -> None: def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None: terminalreporter.write_sep("-", "generated xml file: {}".format(self.logfile)) - def add_global_property(self, name, value): + def add_global_property(self, name: str, value: str) -> None: __tracebackhide__ = True _check_record_param_type("name", name) self.global_properties.append((name, bin_xml_escape(value))) - def _get_global_properties_node(self): + def _get_global_properties_node(self) -> Union[py.xml.Tag, str]: """Return a Junit node containing custom properties, if any. """ if self.global_properties: diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 4fdf1df7435..eaa48e5de36 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,7 +1,6 @@ import os import warnings from functools import lru_cache -from typing import Any from typing import Callable from typing import Dict from typing import Iterable @@ -618,7 +617,7 @@ def __init__( #: user properties is a list of tuples (name, value) that holds user #: defined properties for this test. - self.user_properties = [] # type: List[Tuple[str, Any]] + self.user_properties = [] # type: List[Tuple[str, object]] def runtest(self) -> None: raise NotImplementedError("runtest must be implemented by Item subclass") From 01797e6370a8a3858ee7e3a914021f7dee8318f2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 316/823] Type annotate _pytest.debugging (a bit) --- .pre-commit-config.yaml | 2 +- src/_pytest/debugging.py | 26 ++++++++++++++++---------- src/_pytest/faulthandler.py | 2 +- src/_pytest/hookspec.py | 6 ++++-- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4f379968e26..dc371720417 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: args: [--remove] - id: check-yaml - id: debug-statements - exclude: _pytest/debugging.py + exclude: _pytest/(debugging|hookspec).py language_version: python3 - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.2 diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 423b20ce3e9..3001db4ec63 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -2,6 +2,9 @@ import argparse import functools import sys +from typing import Generator +from typing import Tuple +from typing import Union from _pytest import outcomes from _pytest.compat import TYPE_CHECKING @@ -15,10 +18,11 @@ from _pytest.reports import BaseReport if TYPE_CHECKING: + from _pytest.capture import CaptureManager from _pytest.runner import CallInfo -def _validate_usepdb_cls(value): +def _validate_usepdb_cls(value: str) -> Tuple[str, str]: """Validate syntax of --pdbcls option.""" try: modname, classname = value.split(":") @@ -70,7 +74,7 @@ def pytest_configure(config: Config) -> None: # NOTE: not using pytest_unconfigure, since it might get called although # pytest_configure was not (if another plugin raises UsageError). - def fin(): + def fin() -> None: ( pdb.set_trace, pytestPDB._pluginmanager, @@ -90,13 +94,13 @@ class pytestPDB: _wrapped_pdb_cls = None @classmethod - def _is_capturing(cls, capman): + def _is_capturing(cls, capman: "CaptureManager") -> Union[str, bool]: if capman: return capman.is_capturing() return False @classmethod - def _import_pdb_cls(cls, capman): + def _import_pdb_cls(cls, capman: "CaptureManager"): if not cls._config: import pdb @@ -135,10 +139,12 @@ def _import_pdb_cls(cls, capman): return wrapped_cls @classmethod - def _get_pdb_wrapper_class(cls, pdb_cls, capman): + def _get_pdb_wrapper_class(cls, pdb_cls, capman: "CaptureManager"): import _pytest.config - class PytestPdbWrapper(pdb_cls): + # Type ignored because mypy doesn't support "dynamic" + # inheritance like this. + class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc] # noqa: F821 _pytest_capman = capman _continued = False @@ -257,7 +263,7 @@ def _init_pdb(cls, method, *args, **kwargs): return _pdb @classmethod - def set_trace(cls, *args, **kwargs): + def set_trace(cls, *args, **kwargs) -> None: """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing.""" frame = sys._getframe().f_back _pdb = cls._init_pdb("set_trace", *args, **kwargs) @@ -276,14 +282,14 @@ def pytest_exception_interact( sys.stdout.write(err) _enter_pdb(node, call.excinfo, report) - def pytest_internalerror(self, excrepr, excinfo): + def pytest_internalerror(self, excrepr, excinfo) -> None: tb = _postmortem_traceback(excinfo) post_mortem(tb) class PdbTrace: @hookimpl(hookwrapper=True) - def pytest_pyfunc_call(self, pyfuncitem): + def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]: wrap_pytest_function_for_tracing(pyfuncitem) yield @@ -358,7 +364,7 @@ def _postmortem_traceback(excinfo): return excinfo._excinfo[2] -def post_mortem(t): +def post_mortem(t) -> None: p = pytestPDB._init_pdb("post_mortem") p.reset() p.interaction(None, t) diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 79936b78f9a..0d969840b3d 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -99,7 +99,7 @@ def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: yield @pytest.hookimpl(tryfirst=True) - def pytest_enter_pdb(self): + def pytest_enter_pdb(self) -> None: """Cancel any traceback dumping due to timeout before entering pdb. """ import faulthandler diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 99f646bd6aa..bcc38f47212 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -15,7 +15,9 @@ from _pytest.compat import TYPE_CHECKING if TYPE_CHECKING: + import pdb import warnings + from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import PytestPluginManager @@ -773,7 +775,7 @@ def pytest_exception_interact( """ -def pytest_enter_pdb(config: "Config", pdb): +def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None: """ called upon pdb.set_trace(), can be used by plugins to take special action just before the python debugger enters in interactive mode. @@ -782,7 +784,7 @@ def pytest_enter_pdb(config: "Config", pdb): """ -def pytest_leave_pdb(config: "Config", pdb): +def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None: """ called when leaving pdb (e.g. with continue after pdb.set_trace()). Can be used by plugins to take special action just after the python From f8bb61ae5b87f8a432ae77f1ffa207046682e23f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 317/823] Type annotate _pytest.warnings --- src/_pytest/hookspec.py | 7 ++++--- src/_pytest/warnings.py | 21 +++++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index bcc38f47212..18a9fb39af9 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: import pdb import warnings + from typing_extensions import Literal from _pytest.config import Config from _pytest.config import ExitCode @@ -675,8 +676,8 @@ def pytest_terminal_summary( @hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK) def pytest_warning_captured( warning_message: "warnings.WarningMessage", - when: str, - item, + when: "Literal['config', 'collect', 'runtest']", + item: "Optional[Item]", location: Optional[Tuple[str, int, str]], ) -> None: """(**Deprecated**) Process a warning captured by the internal pytest warnings plugin. @@ -710,7 +711,7 @@ def pytest_warning_captured( @hookspec(historic=True) def pytest_warning_recorded( warning_message: "warnings.WarningMessage", - when: str, + when: "Literal['config', 'collect', 'runtest']", nodeid: str, location: Optional[Tuple[str, int, str]], ) -> None: diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 622cbb806a2..5cedba2442a 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -4,6 +4,7 @@ from contextlib import contextmanager from functools import lru_cache from typing import Generator +from typing import Optional from typing import Tuple import pytest @@ -15,7 +16,8 @@ from _pytest.terminal import TerminalReporter if TYPE_CHECKING: - from typing_extensions import Type + from typing import Type + from typing_extensions import Literal @lru_cache(maxsize=50) @@ -79,7 +81,12 @@ def pytest_configure(config: Config) -> None: @contextmanager -def catch_warnings_for_item(config, ihook, when, item): +def catch_warnings_for_item( + config: Config, + ihook, + when: "Literal['config', 'collect', 'runtest']", + item: Optional[Item], +) -> Generator[None, None, None]: """ Context manager that catches warnings generated in the contained execution block. @@ -133,11 +140,11 @@ def catch_warnings_for_item(config, ihook, when, item): ) -def warning_record_to_str(warning_message): +def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: """Convert a warnings.WarningMessage to a string.""" warn_msg = warning_message.message msg = warnings.formatwarning( - warn_msg, + str(warn_msg), warning_message.category, warning_message.filename, warning_message.lineno, @@ -175,7 +182,7 @@ def pytest_terminal_summary( @pytest.hookimpl(hookwrapper=True) -def pytest_sessionfinish(session): +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 @@ -183,7 +190,7 @@ def pytest_sessionfinish(session): yield -def _issue_warning_captured(warning, hook, stacklevel): +def _issue_warning_captured(warning: Warning, hook, stacklevel: int) -> None: """ This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: at this point the actual options might not have been set, so we manually trigger the pytest_warning_recorded @@ -196,8 +203,6 @@ def _issue_warning_captured(warning, hook, stacklevel): with warnings.catch_warnings(record=True) as records: warnings.simplefilter("always", type(warning)) warnings.warn(warning, stacklevel=stacklevel) - # Mypy can't infer that record=True means records is not None; help it. - assert records is not None frame = sys._getframe(stacklevel - 1) location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name hook.pytest_warning_captured.call_historic( From 1bd7d025d9fbd48673e02d9a570431280f494c1e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 318/823] Type annotate more of _pytest.fixtures --- src/_pytest/fixtures.py | 124 ++++++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 42 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 1a917874353..7b87fc456c9 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -57,8 +57,9 @@ from _pytest import nodes from _pytest.main import Session - from _pytest.python import Metafunc from _pytest.python import CallSpec2 + from _pytest.python import Function + from _pytest.python import Metafunc _Scope = Literal["session", "package", "module", "class", "function"] @@ -189,29 +190,32 @@ def add_funcarg_pseudo_fixture_def( arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]] else: fixturedef = FixtureDef( - fixturemanager, - "", - argname, - get_direct_param_fixture_func, - arg2scope[argname], - valuelist, - False, - False, + fixturemanager=fixturemanager, + baseid="", + argname=argname, + func=get_direct_param_fixture_func, + scope=arg2scope[argname], + params=valuelist, + unittest=False, + ids=None, ) arg2fixturedefs[argname] = [fixturedef] if node is not None: node._name2pseudofixturedef[argname] = fixturedef -def getfixturemarker(obj): +def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: """ return fixturemarker or None if it doesn't exist or raised exceptions.""" try: - return getattr(obj, "_pytestfixturefunction", None) + fixturemarker = getattr( + obj, "_pytestfixturefunction", None + ) # type: Optional[FixtureFunctionMarker] except TEST_OUTCOME: # some objects raise errors like request (from flask import request) # we don't expect them to be fixture functions return None + return fixturemarker # Parametrized fixture key, helper alias for code below. @@ -334,7 +338,7 @@ def reorder_items_atscope( return items_done -def fillfixtures(function) -> None: +def fillfixtures(function: "Function") -> None: """ fill missing funcargs for a test function. """ warnings.warn(FILLFUNCARGS, stacklevel=2) try: @@ -344,6 +348,7 @@ def fillfixtures(function) -> None: # with the oejskit plugin. It uses classes with funcargs # and we thus have to work a bit to allow this. fm = function.session._fixturemanager + assert function.parent is not None fi = fm.getfixtureinfo(function.parent, function.obj, None) function._fixtureinfo = fi request = function._request = FixtureRequest(function) @@ -866,7 +871,7 @@ def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn": fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) -def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs) -> object: +def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs): yieldctx = is_generator(fixturefunc) if yieldctx: generator = fixturefunc(**kwargs) @@ -896,9 +901,15 @@ def _teardown_yield_fixture(fixturefunc, it) -> None: ) -def _eval_scope_callable(scope_callable, fixture_name: str, config: Config) -> str: +def _eval_scope_callable( + scope_callable: "Callable[[str, Config], _Scope]", + fixture_name: str, + config: Config, +) -> "_Scope": try: - result = scope_callable(fixture_name=fixture_name, config=config) + # Type ignored because there is no typing mechanism to specify + # keyword arguments, currently. + result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg] # noqa: F821 except Exception: raise TypeError( "Error evaluating {} while defining fixture '{}'.\n" @@ -924,10 +935,15 @@ def __init__( baseid, argname: str, func, - scope: str, + scope: "Union[_Scope, Callable[[str, Config], _Scope]]", params: Optional[Sequence[object]], unittest: bool = False, - ids=None, + ids: Optional[ + Union[ + Tuple[Union[None, str, float, int, bool], ...], + Callable[[object], Optional[object]], + ] + ] = None, ) -> None: self._fixturemanager = fixturemanager self.baseid = baseid or "" @@ -935,16 +951,15 @@ def __init__( self.func = func self.argname = argname if callable(scope): - scope = _eval_scope_callable(scope, argname, fixturemanager.config) + scope_ = _eval_scope_callable(scope, argname, fixturemanager.config) + else: + scope_ = scope self.scopenum = scope2index( - scope or "function", + scope_ or "function", descr="Fixture '{}'".format(func.__name__), where=baseid, ) - # The cast is verified by scope2index. - # (Some of the type annotations below are supposed to be inferred, - # but mypy 0.761 has some trouble without them.) - self.scope = cast("_Scope", scope) # type: _Scope + self.scope = scope_ self.params = params # type: Optional[Sequence[object]] self.argnames = getfuncargnames( func, name=argname, is_method=unittest @@ -1068,9 +1083,21 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request: SubRequest) -> object: return result -def _ensure_immutable_ids(ids): +def _ensure_immutable_ids( + ids: Optional[ + Union[ + Iterable[Union[None, str, float, int, bool]], + Callable[[object], Optional[object]], + ] + ], +) -> Optional[ + Union[ + Tuple[Union[None, str, float, int, bool], ...], + Callable[[object], Optional[object]], + ] +]: if ids is None: - return + return None if callable(ids): return ids return tuple(ids) @@ -1102,10 +1129,16 @@ def result(*args, **kwargs): class FixtureFunctionMarker: scope = attr.ib() params = attr.ib(converter=attr.converters.optional(tuple)) - autouse = attr.ib(default=False) - # Ignore type because of https://github.com/python/mypy/issues/6172. - ids = attr.ib(default=None, converter=_ensure_immutable_ids) # type: ignore - name = attr.ib(default=None) + autouse = attr.ib(type=bool, default=False) + ids = attr.ib( + type=Union[ + Tuple[Union[None, str, float, int, bool], ...], + Callable[[object], Optional[object]], + ], + default=None, + converter=_ensure_immutable_ids, + ) + name = attr.ib(type=Optional[str], default=None) def __call__(self, function): if inspect.isclass(function): @@ -1133,12 +1166,17 @@ def __call__(self, function): def fixture( fixture_function=None, - *args, - scope="function", + *args: Any, + scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function", params=None, - autouse=False, - ids=None, - name=None + autouse: bool = False, + ids: Optional[ + Union[ + Iterable[Union[None, str, float, int, bool]], + Callable[[object], Optional[object]], + ] + ] = None, + name: Optional[str] = None ): """Decorator to mark a fixture factory function. @@ -1343,7 +1381,7 @@ def __init__(self, session: "Session") -> None: ] # type: List[Tuple[str, List[str]]] session.config.pluginmanager.register(self, "funcmanage") - def _get_direct_parametrize_args(self, node) -> List[str]: + def _get_direct_parametrize_args(self, node: "nodes.Node") -> List[str]: """This function returns all the direct parametrization arguments of a node, so we don't mistake them for fixtures @@ -1362,7 +1400,9 @@ def _get_direct_parametrize_args(self, node) -> List[str]: return parametrize_argnames - def getfixtureinfo(self, node, func, cls, funcargs: bool = True) -> FuncFixtureInfo: + def getfixtureinfo( + self, node: "nodes.Node", func, cls, funcargs: bool = True + ) -> FuncFixtureInfo: if funcargs and not getattr(node, "nofuncargs", False): argnames = getfuncargnames(func, name=node.name, cls=cls) else: @@ -1526,12 +1566,12 @@ def parsefactories( obj = get_real_method(obj, holderobj) fixture_def = FixtureDef( - self, - nodeid, - name, - obj, - marker.scope, - marker.params, + fixturemanager=self, + baseid=nodeid, + argname=name, + func=obj, + scope=marker.scope, + params=marker.params, unittest=unittest, ids=marker.ids, ) From 8bcf1d6de1ba6cb0810a58f9949f9a8db26ad1de Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 319/823] Remove duplicated conversion of pytest.fixture() params argument The FixtureFunctionMarker attrs class already converts the params itself. When adding types, the previous converter composition causes some type error, but extracting it to a standalone function fixes the issue (a lambda is not supported by the mypy plugin, currently). --- src/_pytest/fixtures.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 7b87fc456c9..b0049f8cc89 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1103,6 +1103,12 @@ def _ensure_immutable_ids( return tuple(ids) +def _params_converter( + params: Optional[Iterable[object]], +) -> Optional[Tuple[object, ...]]: + return tuple(params) if params is not None else None + + def wrap_function_to_error_out_if_called_directly(function, fixture_marker): """Wrap the given fixture function so we can raise an error about it being called directly, instead of used as an argument in a test function. @@ -1127,8 +1133,8 @@ def result(*args, **kwargs): @attr.s(frozen=True) class FixtureFunctionMarker: - scope = attr.ib() - params = attr.ib(converter=attr.converters.optional(tuple)) + scope = attr.ib(type="Union[_Scope, Callable[[str, Config], _Scope]]") + params = attr.ib(type=Optional[Tuple[object, ...]], converter=_params_converter) autouse = attr.ib(type=bool, default=False) ids = attr.ib( type=Union[ @@ -1168,7 +1174,7 @@ def fixture( fixture_function=None, *args: Any, scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function", - params=None, + params: Optional[Iterable[object]] = None, autouse: bool = False, ids: Optional[ Union[ @@ -1274,9 +1280,6 @@ def fixture( warnings.warn(FIXTURE_POSITIONAL_ARGUMENTS, stacklevel=2) # End backward compatiblity. - if params is not None: - params = list(params) - fixture_marker = FixtureFunctionMarker( scope=scope, params=params, autouse=autouse, ids=ids, name=name, ) From 28338846884687cf57c4add215a84c3c75085ac1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 320/823] Type annotate pytest.fixture and more improvements to _pytest.fixtures --- src/_pytest/fixtures.py | 126 ++++++++++++++++++++++++++++--------- testing/python/fixtures.py | 15 +++-- 2 files changed, 105 insertions(+), 36 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b0049f8cc89..8aa5d73a8ac 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -10,6 +10,8 @@ from typing import Callable from typing import cast from typing import Dict +from typing import Generator +from typing import Generic from typing import Iterable from typing import Iterator from typing import List @@ -17,6 +19,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import TypeVar from typing import Union import attr @@ -37,6 +40,7 @@ from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import order_preserving_dict +from _pytest.compat import overload from _pytest.compat import safe_getattr from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin @@ -64,13 +68,30 @@ _Scope = Literal["session", "package", "module", "class", "function"] -_FixtureCachedResult = Tuple[ - # The result. - Optional[object], - # Cache key. - object, - # Exc info if raised. - Optional[Tuple["Type[BaseException]", BaseException, TracebackType]], +# The value of the fixture -- return/yield of the fixture function (type variable). +_FixtureValue = TypeVar("_FixtureValue") +# The type of the fixture function (type variable). +_FixtureFunction = TypeVar("_FixtureFunction", bound=Callable[..., object]) +# The type of a fixture function (type alias generic in fixture value). +_FixtureFunc = Union[ + Callable[..., _FixtureValue], Callable[..., Generator[_FixtureValue, None, None]] +] +# The type of FixtureDef.cached_result (type alias generic in fixture value). +_FixtureCachedResult = Union[ + Tuple[ + # The result. + _FixtureValue, + # Cache key. + object, + None, + ], + Tuple[ + None, + # Cache key. + object, + # Exc info if raised. + Tuple["Type[BaseException]", BaseException, TracebackType], + ], ] @@ -871,9 +892,13 @@ def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn": fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) -def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs): - yieldctx = is_generator(fixturefunc) - if yieldctx: +def call_fixture_func( + fixturefunc: "_FixtureFunc[_FixtureValue]", request: FixtureRequest, kwargs +) -> _FixtureValue: + if is_generator(fixturefunc): + fixturefunc = cast( + Callable[..., Generator[_FixtureValue, None, None]], fixturefunc + ) generator = fixturefunc(**kwargs) try: fixture_result = next(generator) @@ -884,6 +909,7 @@ def call_fixture_func(fixturefunc, request: FixtureRequest, kwargs): finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator) request.addfinalizer(finalizer) else: + fixturefunc = cast(Callable[..., _FixtureValue], fixturefunc) fixture_result = fixturefunc(**kwargs) return fixture_result @@ -926,7 +952,7 @@ def _eval_scope_callable( return result -class FixtureDef: +class FixtureDef(Generic[_FixtureValue]): """ A container for a factory definition. """ def __init__( @@ -934,7 +960,7 @@ def __init__( fixturemanager: "FixtureManager", baseid, argname: str, - func, + func: "_FixtureFunc[_FixtureValue]", scope: "Union[_Scope, Callable[[str, Config], _Scope]]", params: Optional[Sequence[object]], unittest: bool = False, @@ -966,7 +992,7 @@ def __init__( ) # type: Tuple[str, ...] self.unittest = unittest self.ids = ids - self.cached_result = None # type: Optional[_FixtureCachedResult] + self.cached_result = None # type: Optional[_FixtureCachedResult[_FixtureValue]] self._finalizers = [] # type: List[Callable[[], object]] def addfinalizer(self, finalizer: Callable[[], object]) -> None: @@ -996,7 +1022,7 @@ def finish(self, request: SubRequest) -> None: self.cached_result = None self._finalizers = [] - def execute(self, request: SubRequest): + def execute(self, request: SubRequest) -> _FixtureValue: # get required arguments and register our own finish() # with their finalization for argname in self.argnames: @@ -1008,14 +1034,15 @@ def execute(self, request: SubRequest): my_cache_key = self.cache_key(request) if self.cached_result is not None: - result, cache_key, err = self.cached_result # note: comparison with `==` can fail (or be expensive) for e.g. # numpy arrays (#6497) + cache_key = self.cached_result[1] if my_cache_key is cache_key: - if err is not None: - _, val, tb = err + if self.cached_result[2] is not None: + _, val, tb = self.cached_result[2] raise val.with_traceback(tb) else: + result = self.cached_result[0] return result # we have a previous but differently parametrized fixture instance # so we need to tear it down before creating a new one @@ -1023,7 +1050,8 @@ def execute(self, request: SubRequest): assert self.cached_result is None hook = self._fixturemanager.session.gethookproxy(request.node.fspath) - return hook.pytest_fixture_setup(fixturedef=self, request=request) + result = hook.pytest_fixture_setup(fixturedef=self, request=request) + return result def cache_key(self, request: SubRequest) -> object: return request.param_index if not hasattr(request, "param") else request.param @@ -1034,7 +1062,9 @@ def __repr__(self) -> str: ) -def resolve_fixture_function(fixturedef: FixtureDef, request: FixtureRequest): +def resolve_fixture_function( + fixturedef: FixtureDef[_FixtureValue], request: FixtureRequest +) -> "_FixtureFunc[_FixtureValue]": """Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific instances and bound methods. """ @@ -1042,7 +1072,7 @@ def resolve_fixture_function(fixturedef: FixtureDef, request: FixtureRequest): if fixturedef.unittest: if request.instance is not None: # bind the unbound method to the TestCase instance - fixturefunc = fixturedef.func.__get__(request.instance) + fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr] # noqa: F821 else: # the fixture function needs to be bound to the actual # request.instance so that code working with "fixturedef" behaves @@ -1051,16 +1081,18 @@ def resolve_fixture_function(fixturedef: FixtureDef, request: FixtureRequest): # handle the case where fixture is defined not in a test class, but some other class # (for example a plugin class with a fixture), see #2270 if hasattr(fixturefunc, "__self__") and not isinstance( - request.instance, fixturefunc.__self__.__class__ + request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr] # noqa: F821 ): return fixturefunc fixturefunc = getimfunc(fixturedef.func) if fixturefunc != fixturedef.func: - fixturefunc = fixturefunc.__get__(request.instance) + fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr] # noqa: F821 return fixturefunc -def pytest_fixture_setup(fixturedef: FixtureDef, request: SubRequest) -> object: +def pytest_fixture_setup( + fixturedef: FixtureDef[_FixtureValue], request: SubRequest +) -> _FixtureValue: """ Execution of fixture setup. """ kwargs = {} for argname in fixturedef.argnames: @@ -1146,7 +1178,7 @@ class FixtureFunctionMarker: ) name = attr.ib(type=Optional[str], default=None) - def __call__(self, function): + def __call__(self, function: _FixtureFunction) -> _FixtureFunction: if inspect.isclass(function): raise ValueError("class fixtures not supported (maybe in the future)") @@ -1166,12 +1198,50 @@ def __call__(self, function): ), pytrace=False, ) - function._pytestfixturefunction = self + + # Type ignored because https://github.com/python/mypy/issues/2087. + function._pytestfixturefunction = self # type: ignore[attr-defined] # noqa: F821 return function +@overload def fixture( - fixture_function=None, + fixture_function: _FixtureFunction, + *, + scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ..., + params: Optional[Iterable[object]] = ..., + autouse: bool = ..., + ids: Optional[ + Union[ + Iterable[Union[None, str, float, int, bool]], + Callable[[object], Optional[object]], + ] + ] = ..., + name: Optional[str] = ... +) -> _FixtureFunction: + raise NotImplementedError() + + +@overload # noqa: F811 +def fixture( # noqa: F811 + fixture_function: None = ..., + *, + scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ..., + params: Optional[Iterable[object]] = ..., + autouse: bool = ..., + ids: Optional[ + Union[ + Iterable[Union[None, str, float, int, bool]], + Callable[[object], Optional[object]], + ] + ] = ..., + name: Optional[str] = None +) -> FixtureFunctionMarker: + raise NotImplementedError() + + +def fixture( # noqa: F811 + fixture_function: Optional[_FixtureFunction] = None, *args: Any, scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function", params: Optional[Iterable[object]] = None, @@ -1183,7 +1253,7 @@ def fixture( ] ] = None, name: Optional[str] = None -): +) -> Union[FixtureFunctionMarker, _FixtureFunction]: """Decorator to mark a fixture factory function. This decorator can be used, with or without parameters, to define a @@ -1317,7 +1387,7 @@ def yield_fixture( @fixture(scope="session") -def pytestconfig(request: FixtureRequest): +def pytestconfig(request: FixtureRequest) -> Config: """Session-scoped fixture that returns the :class:`_pytest.config.Config` object. Example:: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 7fc87e38754..353ce46cd6b 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3799,7 +3799,7 @@ def test_func(m1): request = FixtureRequest(items[0]) assert request.fixturenames == "m1 f1".split() - def test_func_closure_with_native_fixtures(self, testdir, monkeypatch): + def test_func_closure_with_native_fixtures(self, testdir, monkeypatch) -> None: """Sanity check that verifies the order returned by the closures and the actual fixture execution order: The execution order may differ because of fixture inter-dependencies. """ @@ -3849,9 +3849,8 @@ def test_foo(f1, p1, m1, f2, s1): pass ) testdir.runpytest() # actual fixture execution differs: dependent fixtures must be created first ("my_tmpdir") - assert ( - pytest.FIXTURE_ORDER == "s1 my_tmpdir_factory p1 m1 my_tmpdir f1 f2".split() - ) + FIXTURE_ORDER = pytest.FIXTURE_ORDER # type: ignore[attr-defined] # noqa: F821 + assert FIXTURE_ORDER == "s1 my_tmpdir_factory p1 m1 my_tmpdir f1 f2".split() def test_func_closure_module(self, testdir): testdir.makepyfile( @@ -4159,7 +4158,7 @@ def test_fixture_duplicated_arguments() -> None: """Raise error if there are positional and keyword arguments for the same parameter (#1682).""" with pytest.raises(TypeError) as excinfo: - @pytest.fixture("session", scope="session") + @pytest.fixture("session", scope="session") # type: ignore[call-overload] # noqa: F821 def arg(arg): pass @@ -4171,7 +4170,7 @@ def arg(arg): with pytest.raises(TypeError) as excinfo: - @pytest.fixture( + @pytest.fixture( # type: ignore[call-overload] # noqa: F821 "function", ["p1"], True, @@ -4199,7 +4198,7 @@ def test_fixture_with_positionals() -> None: with pytest.warns(pytest.PytestDeprecationWarning) as warnings: - @pytest.fixture("function", [0], True) + @pytest.fixture("function", [0], True) # type: ignore[call-overload] # noqa: F821 def fixture_with_positionals(): pass @@ -4213,7 +4212,7 @@ def fixture_with_positionals(): def test_fixture_with_too_many_positionals() -> None: with pytest.raises(TypeError) as excinfo: - @pytest.fixture("function", [0], True, ["id"], "name", "extra") + @pytest.fixture("function", [0], True, ["id"], "name", "extra") # type: ignore[call-overload] # noqa: F821 def fixture_with_positionals(): pass From c0af19d8ad30840a8ef3c0aedd436816fc86ad3a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 321/823] Type annotate more of _pytest.terminal --- src/_pytest/terminal.py | 185 +++++++++++++++++++++++++--------------- 1 file changed, 114 insertions(+), 71 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 1b9601a2215..b37828e5a5c 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -16,7 +16,9 @@ from typing import List from typing import Mapping from typing import Optional +from typing import Sequence from typing import Set +from typing import TextIO from typing import Tuple from typing import Union @@ -37,11 +39,15 @@ from _pytest.config import ExitCode from _pytest.config.argparsing import Parser from _pytest.deprecated import TERMINALWRITER_WRITER +from _pytest.nodes import Item +from _pytest.nodes import Node from _pytest.reports import BaseReport from _pytest.reports import CollectReport from _pytest.reports import TestReport if TYPE_CHECKING: + from typing_extensions import Literal + from _pytest.main import Session @@ -69,7 +75,14 @@ class MoreQuietAction(argparse.Action): used to unify verbosity handling """ - def __init__(self, option_strings, dest, default=None, required=False, help=None): + def __init__( + self, + option_strings: Sequence[str], + dest: str, + default: object = None, + required: bool = False, + help: Optional[str] = None, + ) -> None: super().__init__( option_strings=option_strings, dest=dest, @@ -79,7 +92,13 @@ def __init__(self, option_strings, dest, default=None, required=False, help=None help=help, ) - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[object], None], + option_string: Optional[str] = None, + ) -> None: new_count = getattr(namespace, self.dest, 0) - 1 setattr(namespace, self.dest, new_count) # todo Deprecate config.quiet @@ -194,7 +213,7 @@ def mywriter(tags, args): def getreportopt(config: Config) -> str: - reportchars = config.option.reportchars + reportchars = config.option.reportchars # type: str old_aliases = {"F", "S"} reportopts = "" @@ -247,10 +266,12 @@ class WarningReport: message = attr.ib(type=str) nodeid = attr.ib(type=Optional[str], default=None) - fslocation = attr.ib(default=None) + fslocation = attr.ib( + type=Optional[Union[Tuple[str, int], py.path.local]], default=None + ) count_towards_summary = True - def get_location(self, config): + def get_location(self, config: Config) -> Optional[str]: """ Returns the more user-friendly information about the location of a warning, or None. @@ -270,13 +291,13 @@ def get_location(self, config): class TerminalReporter: - def __init__(self, config: Config, file=None) -> None: + def __init__(self, config: Config, file: Optional[TextIO] = None) -> None: import _pytest.config self.config = config self._numcollected = 0 self._session = None # type: Optional[Session] - self._showfspath = None + self._showfspath = None # type: Optional[bool] self.stats = {} # type: Dict[str, List[Any]] self._main_color = None # type: Optional[str] @@ -293,6 +314,7 @@ def __init__(self, config: Config, file=None) -> None: self._progress_nodeids_reported = set() # type: Set[str] self._show_progress_info = self._determine_show_progress_info() self._collect_report_last_write = None # type: Optional[float] + self._already_displayed_warnings = None # type: Optional[int] @property def writer(self) -> TerminalWriter: @@ -300,11 +322,11 @@ def writer(self) -> TerminalWriter: return self._tw @writer.setter - def writer(self, value: TerminalWriter): + def writer(self, value: TerminalWriter) -> None: warnings.warn(TERMINALWRITER_WRITER, stacklevel=2) self._tw = value - def _determine_show_progress_info(self): + def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]": """Return True if we should display progress information based on the current config""" # do not show progress if we are not capturing output (#3038) if self.config.getoption("capture", "no") == "no": @@ -312,38 +334,42 @@ def _determine_show_progress_info(self): # do not show progress if we are showing fixture setup/teardown if self.config.getoption("setupshow", False): return False - cfg = self.config.getini("console_output_style") - if cfg in ("progress", "count"): - return cfg - return False + cfg = self.config.getini("console_output_style") # type: str + if cfg == "progress": + return "progress" + elif cfg == "count": + return "count" + else: + return False @property - def verbosity(self): - return self.config.option.verbose + def verbosity(self) -> int: + verbosity = self.config.option.verbose # type: int + return verbosity @property - def showheader(self): + def showheader(self) -> bool: return self.verbosity >= 0 @property - def showfspath(self): + def showfspath(self) -> bool: if self._showfspath is None: return self.verbosity >= 0 return self._showfspath @showfspath.setter - def showfspath(self, value): + def showfspath(self, value: Optional[bool]) -> None: self._showfspath = value @property - def showlongtestinfo(self): + def showlongtestinfo(self) -> bool: return self.verbosity > 0 - def hasopt(self, char): + def hasopt(self, char: str) -> bool: char = {"xfailed": "x", "skipped": "s"}.get(char, char) return char in self.reportchars - def write_fspath_result(self, nodeid, res, **markup): + def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None: fspath = self.config.rootdir.join(nodeid.split("::")[0]) # NOTE: explicitly check for None to work around py bug, and for less # overhead in general (https://github.com/pytest-dev/py/pull/207). @@ -356,7 +382,7 @@ def write_fspath_result(self, nodeid, res, **markup): self._tw.write(fspath + " ") self._tw.write(res, flush=True, **markup) - def write_ensure_prefix(self, prefix, extra="", **kwargs): + def write_ensure_prefix(self, prefix, extra: str = "", **kwargs) -> None: if self.currentfspath != prefix: self._tw.line() self.currentfspath = prefix @@ -376,13 +402,13 @@ def write(self, content: str, *, flush: bool = False, **markup: bool) -> None: def flush(self) -> None: self._tw.flush() - def write_line(self, line: Union[str, bytes], **markup) -> None: + def write_line(self, line: Union[str, bytes], **markup: bool) -> None: if not isinstance(line, str): line = str(line, errors="replace") self.ensure_newline() self._tw.line(line, **markup) - def rewrite(self, line, **markup): + def rewrite(self, line: str, **markup: bool) -> None: """ Rewinds the terminal cursor to the beginning and writes the given line. @@ -400,14 +426,20 @@ def rewrite(self, line, **markup): line = str(line) self._tw.write("\r" + line + fill, **markup) - def write_sep(self, sep, title=None, **markup): + def write_sep( + self, + sep: str, + title: Optional[str] = None, + fullwidth: Optional[int] = None, + **markup: bool + ) -> None: self.ensure_newline() - self._tw.sep(sep, title, **markup) + self._tw.sep(sep, title, fullwidth, **markup) - def section(self, title, sep="=", **kw): + def section(self, title: str, sep: str = "=", **kw: bool) -> None: self._tw.sep(sep, title, **kw) - def line(self, msg, **kw): + def line(self, msg: str, **kw: bool) -> None: self._tw.line(msg, **kw) def _add_stats(self, category: str, items: List) -> None: @@ -421,7 +453,9 @@ def pytest_internalerror(self, excrepr): self.write_line("INTERNALERROR> " + line) return 1 - def pytest_warning_recorded(self, warning_message, nodeid): + def pytest_warning_recorded( + self, warning_message: warnings.WarningMessage, nodeid: str, + ) -> None: from _pytest.warnings import warning_record_to_str fslocation = warning_message.filename, warning_message.lineno @@ -440,10 +474,10 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: # which garbles our output if we use self.write_line self.write_line(msg) - def pytest_deselected(self, items): + def pytest_deselected(self, items) -> None: self._add_stats("deselected", items) - def pytest_runtest_logstart(self, nodeid, location): + def pytest_runtest_logstart(self, nodeid, location) -> None: # ensure that the path is printed before the # 1st test of a module starts running if self.showlongtestinfo: @@ -457,7 +491,9 @@ def pytest_runtest_logstart(self, nodeid, location): def pytest_runtest_logreport(self, report: TestReport) -> None: self._tests_ran = True rep = report - res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config) + res = self.config.hook.pytest_report_teststatus( + report=rep, config=self.config + ) # type: Tuple[str, str, str] category, letter, word = res if isinstance(word, tuple): word, markup = word @@ -504,10 +540,11 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: self.flush() @property - def _is_last_item(self): + def _is_last_item(self) -> bool: + assert self._session is not None return len(self._progress_nodeids_reported) == self._session.testscollected - def pytest_runtest_logfinish(self, nodeid): + def pytest_runtest_logfinish(self, nodeid) -> None: assert self._session if self.verbosity <= 0 and self._show_progress_info: if self._show_progress_info == "count": @@ -545,7 +582,7 @@ def _get_progress_information_message(self) -> str: ) return " [100%]" - def _write_progress_information_filling_space(self): + def _write_progress_information_filling_space(self) -> None: color, _ = self._get_main_color() msg = self._get_progress_information_message() w = self._width_of_current_line @@ -553,7 +590,7 @@ def _write_progress_information_filling_space(self): self.write(msg.rjust(fill), flush=True, **{color: True}) @property - def _width_of_current_line(self): + def _width_of_current_line(self) -> int: """Return the width of current line, using the superior implementation of py-1.6 when available""" return self._tw.width_of_current_line @@ -575,7 +612,7 @@ def pytest_collectreport(self, report: CollectReport) -> None: if self.isatty: self.report_collect() - def report_collect(self, final=False): + def report_collect(self, final: bool = False) -> None: if self.config.option.verbose < 0: return @@ -643,7 +680,9 @@ def pytest_sessionstart(self, session: "Session") -> None: ) self._write_report_lines_from_hooks(lines) - def _write_report_lines_from_hooks(self, lines) -> None: + def _write_report_lines_from_hooks( + self, lines: List[Union[str, List[str]]] + ) -> None: lines.reverse() for line in collapse(lines): self.write_line(line) @@ -685,7 +724,7 @@ def pytest_collection_finish(self, session: "Session") -> None: for rep in failed: rep.toterminal(self._tw) - def _printcollecteditems(self, items): + def _printcollecteditems(self, items: Sequence[Item]) -> None: # to print out items and their parent collectors # we take care to leave out Instances aka () # because later versions are going to get rid of them anyway @@ -701,7 +740,7 @@ def _printcollecteditems(self, items): for item in items: self._tw.line(item.nodeid) return - stack = [] + stack = [] # type: List[Node] indent = "" for item in items: needed_collectors = item.listchain()[1:] # strip root node @@ -716,11 +755,8 @@ def _printcollecteditems(self, items): indent = (len(stack) - 1) * " " self._tw.line("{}{}".format(indent, col)) if self.config.option.verbose >= 1: - try: - obj = col.obj # type: ignore - except AttributeError: - continue - doc = inspect.getdoc(obj) + obj = getattr(col, "obj", None) + doc = inspect.getdoc(obj) if obj else None if doc: for line in doc.splitlines(): self._tw.line("{}{}".format(indent + " ", line)) @@ -744,12 +780,12 @@ def pytest_sessionfinish( terminalreporter=self, exitstatus=exitstatus, config=self.config ) if session.shouldfail: - self.write_sep("!", session.shouldfail, red=True) + self.write_sep("!", str(session.shouldfail), red=True) if exitstatus == ExitCode.INTERRUPTED: self._report_keyboardinterrupt() del self._keyboardinterrupt_memo elif session.shouldstop: - self.write_sep("!", session.shouldstop, red=True) + self.write_sep("!", str(session.shouldstop), red=True) self.summary_stats() @pytest.hookimpl(hookwrapper=True) @@ -770,7 +806,7 @@ def pytest_unconfigure(self) -> None: if hasattr(self, "_keyboardinterrupt_memo"): self._report_keyboardinterrupt() - def _report_keyboardinterrupt(self): + def _report_keyboardinterrupt(self) -> None: excrepr = self._keyboardinterrupt_memo msg = excrepr.reprcrash.message self.write_sep("!", msg) @@ -824,14 +860,14 @@ def _getcrashline(self, rep): # # summaries for sessionfinish # - def getreports(self, name): + def getreports(self, name: str): values = [] for x in self.stats.get(name, []): if not hasattr(x, "_pdbshown"): values.append(x) return values - def summary_warnings(self): + def summary_warnings(self) -> None: if self.hasopt("w"): all_warnings = self.stats.get( "warnings" @@ -839,7 +875,7 @@ def summary_warnings(self): if not all_warnings: return - final = hasattr(self, "_already_displayed_warnings") + final = self._already_displayed_warnings is not None if final: warning_reports = all_warnings[self._already_displayed_warnings :] else: @@ -854,7 +890,7 @@ def summary_warnings(self): for wr in warning_reports: reports_grouped_by_message.setdefault(wr.message, []).append(wr) - def collapsed_location_report(reports: List[WarningReport]): + def collapsed_location_report(reports: List[WarningReport]) -> str: locations = [] for w in reports: location = w.get_location(self.config) @@ -888,10 +924,10 @@ def collapsed_location_report(reports: List[WarningReport]): self._tw.line() self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html") - def summary_passes(self): + def summary_passes(self) -> None: if self.config.option.tbstyle != "no": if self.hasopt("P"): - reports = self.getreports("passed") + reports = self.getreports("passed") # type: List[TestReport] if not reports: return self.write_sep("=", "PASSES") @@ -903,9 +939,10 @@ def summary_passes(self): self._handle_teardown_sections(rep.nodeid) def _get_teardown_reports(self, nodeid: str) -> List[TestReport]: + reports = self.getreports("") return [ report - for report in self.getreports("") + for report in reports if report.when == "teardown" and report.nodeid == nodeid ] @@ -926,9 +963,9 @@ def print_teardown_sections(self, rep: TestReport) -> None: content = content[:-1] self._tw.line(content) - def summary_failures(self): + def summary_failures(self) -> None: if self.config.option.tbstyle != "no": - reports = self.getreports("failed") + reports = self.getreports("failed") # type: List[BaseReport] if not reports: return self.write_sep("=", "FAILURES") @@ -943,9 +980,9 @@ def summary_failures(self): self._outrep_summary(rep) self._handle_teardown_sections(rep.nodeid) - def summary_errors(self): + def summary_errors(self) -> None: if self.config.option.tbstyle != "no": - reports = self.getreports("error") + reports = self.getreports("error") # type: List[BaseReport] if not reports: return self.write_sep("=", "ERRORS") @@ -958,7 +995,7 @@ def summary_errors(self): self.write_sep("_", msg, red=True, bold=True) self._outrep_summary(rep) - def _outrep_summary(self, rep): + def _outrep_summary(self, rep: BaseReport) -> None: rep.toterminal(self._tw) showcapture = self.config.option.showcapture if showcapture == "no": @@ -971,7 +1008,7 @@ def _outrep_summary(self, rep): content = content[:-1] self._tw.line(content) - def summary_stats(self): + def summary_stats(self) -> None: if self.verbosity < -1: return @@ -1041,7 +1078,7 @@ def show_xpassed(lines: List[str]) -> None: lines.append("{} {} {}".format(verbose_word, pos, reason)) def show_skipped(lines: List[str]) -> None: - skipped = self.stats.get("skipped", []) + skipped = self.stats.get("skipped", []) # type: List[CollectReport] fskips = _folded_skips(self.startdir, skipped) if skipped else [] if not fskips: return @@ -1125,12 +1162,14 @@ def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], s return parts, main_color -def _get_pos(config, rep): +def _get_pos(config: Config, rep: BaseReport): nodeid = config.cwd_relative_nodeid(rep.nodeid) return nodeid -def _get_line_with_reprcrash_message(config, rep, termwidth): +def _get_line_with_reprcrash_message( + config: Config, rep: BaseReport, termwidth: int +) -> str: """Get summary line for a report, trying to add reprcrash message.""" verbose_word = rep._get_verbose_word(config) pos = _get_pos(config, rep) @@ -1143,7 +1182,8 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): return line try: - msg = rep.longrepr.reprcrash.message + # Type ignored intentionally -- possible AttributeError expected. + msg = rep.longrepr.reprcrash.message # type: ignore[union-attr] # noqa: F821 except AttributeError: pass else: @@ -1166,9 +1206,12 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): return line -def _folded_skips(startdir, skipped): - d = {} +def _folded_skips( + startdir: py.path.local, skipped: Sequence[CollectReport], +) -> List[Tuple[int, str, Optional[int], str]]: + d = {} # type: Dict[Tuple[str, Optional[int], str], List[CollectReport]] for event in skipped: + assert event.longrepr is not None assert len(event.longrepr) == 3, (event, event.longrepr) fspath, lineno, reason = event.longrepr # For consistency, report all fspaths in relative form. @@ -1182,13 +1225,13 @@ def _folded_skips(startdir, skipped): and "skip" in keywords and "pytestmark" not in keywords ): - key = (fspath, None, reason) + key = (fspath, None, reason) # type: Tuple[str, Optional[int], str] else: key = (fspath, lineno, reason) d.setdefault(key, []).append(event) - values = [] + values = [] # type: List[Tuple[int, str, Optional[int], str]] for key, events in d.items(): - values.append((len(events),) + key) + values.append((len(events), *key)) return values @@ -1201,7 +1244,7 @@ def _folded_skips(startdir, skipped): _color_for_type_default = "yellow" -def _make_plural(count, noun): +def _make_plural(count: int, noun: str) -> Tuple[int, str]: # No need to pluralize words such as `failed` or `passed`. if noun not in ["error", "warnings"]: return count, noun From 848ab00663c9daf8cd27ee92dec1005cd9633152 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 322/823] Type annotate `@pytest.mark.foo` --- src/_pytest/mark/structures.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 7ae7d5d4fb1..7abff9b7b83 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -3,6 +3,7 @@ import typing import warnings from typing import Any +from typing import Callable from typing import Iterable from typing import List from typing import Mapping @@ -11,6 +12,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import TypeVar from typing import Union import attr @@ -19,6 +21,7 @@ from ..compat import ascii_escaped from ..compat import NOTSET from ..compat import NotSetType +from ..compat import overload from ..compat import TYPE_CHECKING from _pytest.config import Config from _pytest.outcomes import fail @@ -240,6 +243,12 @@ def combined_with(self, other: "Mark") -> "Mark": ) +# A generic parameter designating an object to which a Mark may +# be applied -- a test function (callable) or class. +# Note: a lambda is not allowed, but this can't be represented. +_Markable = TypeVar("_Markable", bound=Union[Callable[..., object], type]) + + @attr.s class MarkDecorator: """A decorator for applying a mark on test functions and classes. @@ -311,7 +320,20 @@ def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": mark = Mark(self.name, args, kwargs) return self.__class__(self.mark.combined_with(mark)) - def __call__(self, *args: object, **kwargs: object): + # Type ignored because the overloads overlap with an incompatible + # return type. Not much we can do about that. Thankfully mypy picks + # the first match so it works out even if we break the rules. + @overload + def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc] # noqa: F821 + raise NotImplementedError() + + @overload # noqa: F811 + def __call__( # noqa: F811 + self, *args: object, **kwargs: object + ) -> "MarkDecorator": + raise NotImplementedError() + + def __call__(self, *args: object, **kwargs: object): # noqa: F811 """Call the MarkDecorator.""" if args and not kwargs: func = args[0] From 71dfdca4df6961460653c265026e194fbcaebef2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:16 +0300 Subject: [PATCH 323/823] Enable check_untyped_defs mypy option for src/ This option checks even functions which are not annotated. It's a good step to ensure that existing type annotation are correct. In a Pareto fashion, the last few holdouts are always the ugliest, beware. --- setup.cfg | 3 +++ src/_pytest/capture.py | 8 +++++--- src/_pytest/config/__init__.py | 6 ++++-- src/_pytest/fixtures.py | 6 ++++-- src/_pytest/nodes.py | 36 ++++++++++++++++++---------------- src/_pytest/pytester.py | 2 ++ src/_pytest/python.py | 26 +++++++++++++++++++++--- src/_pytest/python_api.py | 4 ++-- src/_pytest/recwarn.py | 5 +++-- 9 files changed, 65 insertions(+), 31 deletions(-) diff --git a/setup.cfg b/setup.cfg index a7dd6d1c310..a42ae68aec7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -98,3 +98,6 @@ strict_equality = True warn_redundant_casts = True warn_return_any = True warn_unused_configs = True + +[mypy-_pytest.*] +check_untyped_defs = True diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index bcc16ceb6af..98ba878b3f0 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -519,10 +519,11 @@ def start_capturing(self) -> None: def pop_outerr_to_orig(self): """ pop current snapshot out/err capture and flush to orig streams. """ out, err = self.readouterr() + # TODO: Fix type ignores. if out: - self.out.writeorg(out) + self.out.writeorg(out) # type: ignore[union-attr] # noqa: F821 if err: - self.err.writeorg(err) + self.err.writeorg(err) # type: ignore[union-attr] # noqa: F821 return out, err def suspend_capturing(self, in_: bool = False) -> None: @@ -542,7 +543,8 @@ def resume_capturing(self) -> None: if self.err: self.err.resume() if self._in_suspended: - self.in_.resume() + # TODO: Fix type ignore. + self.in_.resume() # type: ignore[union-attr] # noqa: F821 self._in_suspended = False def stop_capturing(self) -> None: diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ff6aee744e3..27083900dfa 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -974,7 +974,7 @@ def _consider_importhook(self, args: Sequence[str]) -> None: self._mark_plugins_for_rewrite(hook) _warn_about_missing_assertion(mode) - def _mark_plugins_for_rewrite(self, hook): + def _mark_plugins_for_rewrite(self, hook) -> None: """ Given an importhook, mark for rewrite any top-level modules or packages in the distribution package for @@ -989,7 +989,9 @@ def _mark_plugins_for_rewrite(self, hook): package_files = ( str(file) for dist in importlib_metadata.distributions() - if any(ep.group == "pytest11" for ep in dist.entry_points) + # Type ignored due to missing stub: + # https://github.com/python/typeshed/pull/3795 + if any(ep.group == "pytest11" for ep in dist.entry_points) # type: ignore for file in dist.files or [] ) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 8aa5d73a8ac..fa7e3e1df16 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -721,7 +721,9 @@ def _getscopeitem(self, scope): # this might also be a non-function Item despite its attribute name return self._pyfuncitem if scope == "package": - node = get_scope_package(self._pyfuncitem, self._fixturedef) + # FIXME: _fixturedef is not defined on FixtureRequest (this class), + # but on FixtureRequest (a subclass). + node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined] # noqa: F821 else: node = get_scope_node(self._pyfuncitem, scope) if node is None and scope == "class": @@ -1158,7 +1160,7 @@ def result(*args, **kwargs): # keep reference to the original function in our own custom attribute so we don't unwrap # further than this point and lose useful wrappings like @mock.patch (#3774) - result.__pytest_wrapped__ = _PytestWrapper(function) + result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined] # noqa: F821 return result diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index eaa48e5de36..15f91343fae 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -42,6 +42,8 @@ # Imported here due to circular import. from _pytest.main import Session + from _pytest.warning_types import PytestWarning + SEP = "/" @@ -118,9 +120,9 @@ class Node(metaclass=NodeMeta): def __init__( self, name: str, - parent: Optional["Node"] = None, + parent: "Optional[Node]" = None, config: Optional[Config] = None, - session: Optional["Session"] = None, + session: "Optional[Session]" = None, fspath: Optional[py.path.local] = None, nodeid: Optional[str] = None, ) -> None: @@ -201,7 +203,7 @@ def ihook(self): def __repr__(self) -> str: return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) - def warn(self, warning): + def warn(self, warning: "PytestWarning") -> None: """Issue a warning for this item. Warnings will be displayed after the test session, unless explicitly suppressed @@ -226,11 +228,9 @@ def warn(self, warning): ) ) path, lineno = get_fslocation_from_item(self) + assert lineno is not None warnings.warn_explicit( - warning, - category=None, - filename=str(path), - lineno=lineno + 1 if lineno is not None else None, + warning, category=None, filename=str(path), lineno=lineno + 1, ) # methods for ordering nodes @@ -417,24 +417,26 @@ def repr_failure( def get_fslocation_from_item( - item: "Item", + node: "Node", ) -> Tuple[Union[str, py.path.local], Optional[int]]: - """Tries to extract the actual location from an item, depending on available attributes: + """Tries to extract the actual location from a node, depending on available attributes: - * "fslocation": a pair (path, lineno) - * "obj": a Python object that the item wraps. + * "location": a pair (path, lineno) + * "obj": a Python object that the node wraps. * "fspath": just a path :rtype: a tuple of (str|LocalPath, int) with filename and line number. """ - try: - return item.location[:2] - except AttributeError: - pass - obj = getattr(item, "obj", None) + # See Item.location. + location = getattr( + node, "location", None + ) # type: Optional[Tuple[str, Optional[int], str]] + if location is not None: + return location[:2] + obj = getattr(node, "obj", None) if obj is not None: return getfslineno(obj) - return getattr(item, "fspath", "unknown location"), -1 + return getattr(node, "fspath", "unknown location"), -1 class Collector(Node): diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 60df17b90ac..754ecc10f2d 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1169,8 +1169,10 @@ def popen( popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) if stdin is Testdir.CLOSE_STDIN: + assert popen.stdin is not None popen.stdin.close() elif isinstance(stdin, bytes): + assert popen.stdin is not None popen.stdin.write(stdin) return popen diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 55ed2b164a7..41dd8b292cc 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -64,6 +64,7 @@ from _pytest.warning_types import PytestUnhandledCoroutineWarning if TYPE_CHECKING: + from typing import Type from typing_extensions import Literal from _pytest.fixtures import _Scope @@ -256,6 +257,18 @@ def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj): class PyobjMixin: _ALLOW_MARKERS = True + # Function and attributes that the mixin needs (for type-checking only). + if TYPE_CHECKING: + name = "" # type: str + parent = None # type: Optional[nodes.Node] + own_markers = [] # type: List[Mark] + + def getparent(self, cls: Type[nodes._NodeType]) -> Optional[nodes._NodeType]: + ... + + def listchain(self) -> List[nodes.Node]: + ... + @property def module(self): """Python module object this node was collected from (can be None).""" @@ -292,7 +305,10 @@ def obj(self, value): def _getobj(self): """Gets the underlying Python object. May be overwritten by subclasses.""" - return getattr(self.parent.obj, self.name) + # TODO: Improve the type of `parent` such that assert/ignore aren't needed. + assert self.parent is not None + obj = self.parent.obj # type: ignore[attr-defined] # noqa: F821 + return getattr(obj, self.name) def getmodpath(self, stopatmodule=True, includemodule=False): """ return python path relative to the containing module. """ @@ -772,7 +788,10 @@ class Instance(PyCollector): # can be removed at node structure reorganization time def _getobj(self): - return self.parent.obj() + # TODO: Improve the type of `parent` such that assert/ignore aren't needed. + assert self.parent is not None + obj = self.parent.obj # type: ignore[attr-defined] # noqa: F821 + return obj() def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: self.session._fixturemanager.parsefactories(self) @@ -1527,7 +1546,8 @@ def function(self): return getimfunc(self.obj) def _getobj(self): - return getattr(self.parent.obj, self.originalname) + assert self.parent is not None + return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined] @property def _pyfuncitem(self): diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 29c8af7e281..abace319616 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -508,7 +508,7 @@ def approx(expected, rel=None, abs=None, nan_ok=False): __tracebackhide__ = True if isinstance(expected, Decimal): - cls = ApproxDecimal + cls = ApproxDecimal # type: Type[ApproxBase] elif isinstance(expected, Number): cls = ApproxScalar elif isinstance(expected, Mapping): @@ -534,7 +534,7 @@ def _is_numpy_array(obj): """ import sys - np = sys.modules.get("numpy") + np = sys.modules.get("numpy") # type: Any if np is not None: return isinstance(obj, np.ndarray) return False diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 58b6fbab949..57034be2aca 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -136,8 +136,9 @@ class WarningsRecorder(warnings.catch_warnings): Adapted from `warnings.catch_warnings`. """ - def __init__(self): - super().__init__(record=True) + def __init__(self) -> None: + # Type ignored due to the way typeshed handles warnings.catch_warnings. + super().__init__(record=True) # type: ignore[call-arg] # noqa: F821 self._entered = False self._list = [] # type: List[warnings.WarningMessage] From 54ad048be7182018e70479bd3d9b88bcb6376c00 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:17 +0300 Subject: [PATCH 324/823] Enable check_untyped_defs mypy option for testing/ too --- setup.cfg | 4 +- src/_pytest/_code/code.py | 3 +- src/_pytest/logging.py | 12 +- src/_pytest/pytester.py | 2 +- src/_pytest/python_api.py | 1 + testing/code/test_excinfo.py | 87 +++-- testing/deprecated_test.py | 6 +- .../dataclasses/test_compare_dataclasses.py | 4 +- ...ompare_dataclasses_field_comparison_off.py | 4 +- .../test_compare_dataclasses_verbose.py | 4 +- .../test_compare_two_different_dataclasses.py | 8 +- testing/example_scripts/issue_519.py | 4 +- .../unittest/test_unittest_asyncio.py | 3 +- .../unittest/test_unittest_asynctest.py | 3 +- testing/io/test_saferepr.py | 12 +- testing/logging/test_formatter.py | 15 +- testing/logging/test_reporting.py | 13 +- testing/python/approx.py | 3 +- testing/python/collect.py | 30 +- testing/python/integration.py | 29 +- testing/python/raises.py | 32 +- testing/test_assertion.py | 90 +++-- testing/test_assertrewrite.py | 365 ++++++++++-------- testing/test_capture.py | 6 +- testing/test_collection.py | 3 +- testing/test_config.py | 40 +- testing/test_debugging.py | 8 +- testing/test_doctest.py | 8 +- testing/test_junitxml.py | 37 +- testing/test_mark.py | 17 +- testing/test_monkeypatch.py | 40 +- testing/test_nodes.py | 23 +- testing/test_pastebin.py | 7 +- testing/test_pluginmanager.py | 15 +- testing/test_reports.py | 16 +- testing/test_runner_xunit.py | 8 +- testing/test_skipping.py | 8 +- testing/test_terminal.py | 25 +- testing/test_tmpdir.py | 30 +- testing/test_unittest.py | 9 +- testing/test_warnings.py | 7 +- 41 files changed, 598 insertions(+), 443 deletions(-) diff --git a/setup.cfg b/setup.cfg index a42ae68aec7..5dc778d9991 100644 --- a/setup.cfg +++ b/setup.cfg @@ -91,6 +91,7 @@ formats = sdist.tgz,bdist_wheel [mypy] mypy_path = src +check_untyped_defs = True ignore_missing_imports = True no_implicit_optional = True show_error_codes = True @@ -98,6 +99,3 @@ strict_equality = True warn_redundant_casts = True warn_return_any = True warn_unused_configs = True - -[mypy-_pytest.*] -check_untyped_defs = True diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 7b17d761274..09b2c1af525 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -15,6 +15,7 @@ from typing import Generic from typing import Iterable from typing import List +from typing import Mapping from typing import Optional from typing import Pattern from typing import Sequence @@ -728,7 +729,7 @@ def get_exconly( failindent = indentstr return lines - def repr_locals(self, locals: Dict[str, object]) -> Optional["ReprLocals"]: + def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]: if self.showlocals: lines = [] keys = [loc for loc in locals if loc[0] != "@"] diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index ce3a18f032d..c1f13b701da 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -96,7 +96,7 @@ class PercentStyleMultiline(logging.PercentStyle): formats the message as if each line were logged separately. """ - def __init__(self, fmt: str, auto_indent: Union[int, str, bool]) -> None: + def __init__(self, fmt: str, auto_indent: Union[int, str, bool, None]) -> None: super().__init__(fmt) self._auto_indent = self._get_auto_indent(auto_indent) @@ -109,7 +109,7 @@ def _update_message( return tmp @staticmethod - def _get_auto_indent(auto_indent_option: Union[int, str, bool]) -> int: + def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int: """Determines the current auto indentation setting Specify auto indent behavior (on/off/fixed) by passing in @@ -139,7 +139,9 @@ def _get_auto_indent(auto_indent_option: Union[int, str, bool]) -> int: >0 (explicitly set indentation position). """ - if type(auto_indent_option) is int: + if auto_indent_option is None: + return 0 + elif type(auto_indent_option) is int: return int(auto_indent_option) elif type(auto_indent_option) is str: try: @@ -732,7 +734,9 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): stream = None # type: TerminalReporter # type: ignore def __init__( - self, terminal_reporter: TerminalReporter, capture_manager: CaptureManager + self, + terminal_reporter: TerminalReporter, + capture_manager: Optional[CaptureManager], ) -> None: """ :param _pytest.terminal.TerminalReporter terminal_reporter: diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 754ecc10f2d..8df5992d659 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -401,7 +401,7 @@ def _sys_snapshot(): @pytest.fixture -def _config_for_test(): +def _config_for_test() -> Generator[Config, None, None]: from _pytest.config import get_config config = get_config() diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index abace319616..c185a06766a 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -712,6 +712,7 @@ def raises( # noqa: F811 fail(message) +# This doesn't work with mypy for now. Use fail.Exception instead. raises.Exception = fail.Exception # type: ignore diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 08c0619e3ff..0ff00bcaa8b 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -4,6 +4,7 @@ import queue import sys import textwrap +from typing import Tuple from typing import Union import py @@ -14,6 +15,7 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import FormattedExcinfo from _pytest._io import TerminalWriter +from _pytest.compat import TYPE_CHECKING from _pytest.pytester import LineMatcher try: @@ -23,6 +25,9 @@ else: invalidate_import_caches = getattr(importlib, "invalidate_caches", None) +if TYPE_CHECKING: + from _pytest._code.code import _TracebackStyle + @pytest.fixture def limited_recursion_depth(): @@ -40,10 +45,11 @@ def test_excinfo_simple() -> None: assert info.type == ValueError -def test_excinfo_from_exc_info_simple(): +def test_excinfo_from_exc_info_simple() -> None: try: raise ValueError except ValueError as e: + assert e.__traceback__ is not None info = _pytest._code.ExceptionInfo.from_exc_info((type(e), e, e.__traceback__)) assert info.type == ValueError @@ -317,25 +323,25 @@ def test_excinfo_exconly(): assert msg.endswith("world") -def test_excinfo_repr_str(): - excinfo = pytest.raises(ValueError, h) - assert repr(excinfo) == "" - assert str(excinfo) == "" +def test_excinfo_repr_str() -> None: + excinfo1 = pytest.raises(ValueError, h) + assert repr(excinfo1) == "" + assert str(excinfo1) == "" class CustomException(Exception): def __repr__(self): return "custom_repr" - def raises(): + def raises() -> None: raise CustomException() - excinfo = pytest.raises(CustomException, raises) - assert repr(excinfo) == "" - assert str(excinfo) == "" + excinfo2 = pytest.raises(CustomException, raises) + assert repr(excinfo2) == "" + assert str(excinfo2) == "" -def test_excinfo_for_later(): - e = ExceptionInfo.for_later() +def test_excinfo_for_later() -> None: + e = ExceptionInfo[BaseException].for_later() assert "for raises" in repr(e) assert "for raises" in str(e) @@ -463,7 +469,7 @@ def f(x): assert lines[0] == "| def f(x):" assert lines[1] == " pass" - def test_repr_source_excinfo(self): + def test_repr_source_excinfo(self) -> None: """ check if indentation is right """ pr = FormattedExcinfo() excinfo = self.excinfo_from_exec( @@ -475,6 +481,7 @@ def f(): ) pr = FormattedExcinfo() source = pr._getentrysource(excinfo.traceback[-1]) + assert source is not None lines = pr.get_source(source, 1, excinfo) assert lines == [" def f():", "> assert 0", "E AssertionError"] @@ -522,17 +529,18 @@ def test_repr_source_failing_fullsource(self, monkeypatch) -> None: assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" assert repr.chain[0][0].reprentries[0].lines[0] == "> ???" - def test_repr_local(self): + def test_repr_local(self) -> None: p = FormattedExcinfo(showlocals=True) loc = {"y": 5, "z": 7, "x": 3, "@x": 2, "__builtins__": {}} reprlocals = p.repr_locals(loc) + assert reprlocals is not None assert reprlocals.lines assert reprlocals.lines[0] == "__builtins__ = " assert reprlocals.lines[1] == "x = 3" assert reprlocals.lines[2] == "y = 5" assert reprlocals.lines[3] == "z = 7" - def test_repr_local_with_error(self): + def test_repr_local_with_error(self) -> None: class ObjWithErrorInRepr: def __repr__(self): raise NotImplementedError @@ -540,11 +548,12 @@ def __repr__(self): p = FormattedExcinfo(showlocals=True, truncate_locals=False) loc = {"x": ObjWithErrorInRepr(), "__builtins__": {}} reprlocals = p.repr_locals(loc) + assert reprlocals is not None assert reprlocals.lines assert reprlocals.lines[0] == "__builtins__ = " assert "[NotImplementedError() raised in repr()]" in reprlocals.lines[1] - def test_repr_local_with_exception_in_class_property(self): + def test_repr_local_with_exception_in_class_property(self) -> None: class ExceptionWithBrokenClass(Exception): # Type ignored because it's bypassed intentionally. @property # type: ignore @@ -558,23 +567,26 @@ def __repr__(self): p = FormattedExcinfo(showlocals=True, truncate_locals=False) loc = {"x": ObjWithErrorInRepr(), "__builtins__": {}} reprlocals = p.repr_locals(loc) + assert reprlocals is not None assert reprlocals.lines assert reprlocals.lines[0] == "__builtins__ = " assert "[ExceptionWithBrokenClass() raised in repr()]" in reprlocals.lines[1] - def test_repr_local_truncated(self): + def test_repr_local_truncated(self) -> None: loc = {"l": [i for i in range(10)]} p = FormattedExcinfo(showlocals=True) truncated_reprlocals = p.repr_locals(loc) + assert truncated_reprlocals is not None assert truncated_reprlocals.lines assert truncated_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, ...]" q = FormattedExcinfo(showlocals=True, truncate_locals=False) full_reprlocals = q.repr_locals(loc) + assert full_reprlocals is not None assert full_reprlocals.lines assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" - def test_repr_tracebackentry_lines(self, importasmod): + def test_repr_tracebackentry_lines(self, importasmod) -> None: mod = importasmod( """ def func1(): @@ -602,11 +614,12 @@ def func1(): assert not lines[4:] loc = repr_entry.reprfileloc + assert loc is not None assert loc.path == mod.__file__ assert loc.lineno == 3 # assert loc.message == "ValueError: hello" - def test_repr_tracebackentry_lines2(self, importasmod, tw_mock): + def test_repr_tracebackentry_lines2(self, importasmod, tw_mock) -> None: mod = importasmod( """ def func1(m, x, y, z): @@ -618,6 +631,7 @@ def func1(m, x, y, z): entry = excinfo.traceback[-1] p = FormattedExcinfo(funcargs=True) reprfuncargs = p.repr_args(entry) + assert reprfuncargs is not None assert reprfuncargs.args[0] == ("m", repr("m" * 90)) assert reprfuncargs.args[1] == ("x", "5") assert reprfuncargs.args[2] == ("y", "13") @@ -625,13 +639,14 @@ def func1(m, x, y, z): p = FormattedExcinfo(funcargs=True) repr_entry = p.repr_traceback_entry(entry) + assert repr_entry.reprfuncargs is not None assert repr_entry.reprfuncargs.args == reprfuncargs.args repr_entry.toterminal(tw_mock) assert tw_mock.lines[0] == "m = " + repr("m" * 90) assert tw_mock.lines[1] == "x = 5, y = 13" assert tw_mock.lines[2] == "z = " + repr("z" * 120) - def test_repr_tracebackentry_lines_var_kw_args(self, importasmod, tw_mock): + def test_repr_tracebackentry_lines_var_kw_args(self, importasmod, tw_mock) -> None: mod = importasmod( """ def func1(x, *y, **z): @@ -643,17 +658,19 @@ def func1(x, *y, **z): entry = excinfo.traceback[-1] p = FormattedExcinfo(funcargs=True) reprfuncargs = p.repr_args(entry) + assert reprfuncargs is not None assert reprfuncargs.args[0] == ("x", repr("a")) assert reprfuncargs.args[1] == ("y", repr(("b",))) assert reprfuncargs.args[2] == ("z", repr({"c": "d"})) p = FormattedExcinfo(funcargs=True) repr_entry = p.repr_traceback_entry(entry) + assert repr_entry.reprfuncargs assert repr_entry.reprfuncargs.args == reprfuncargs.args repr_entry.toterminal(tw_mock) assert tw_mock.lines[0] == "x = 'a', y = ('b',), z = {'c': 'd'}" - def test_repr_tracebackentry_short(self, importasmod): + def test_repr_tracebackentry_short(self, importasmod) -> None: mod = importasmod( """ def func1(): @@ -668,6 +685,7 @@ def entry(): lines = reprtb.lines basename = py.path.local(mod.__file__).basename assert lines[0] == " func1()" + assert reprtb.reprfileloc is not None assert basename in str(reprtb.reprfileloc.path) assert reprtb.reprfileloc.lineno == 5 @@ -677,6 +695,7 @@ def entry(): lines = reprtb.lines assert lines[0] == ' raise ValueError("hello")' assert lines[1] == "E ValueError: hello" + assert reprtb.reprfileloc is not None assert basename in str(reprtb.reprfileloc.path) assert reprtb.reprfileloc.lineno == 3 @@ -716,7 +735,7 @@ def entry(): reprtb = p.repr_traceback(excinfo) assert len(reprtb.reprentries) == 3 - def test_traceback_short_no_source(self, importasmod, monkeypatch): + def test_traceback_short_no_source(self, importasmod, monkeypatch) -> None: mod = importasmod( """ def func1(): @@ -729,7 +748,7 @@ def entry(): from _pytest._code.code import Code monkeypatch.setattr(Code, "path", "bogus") - excinfo.traceback[0].frame.code.path = "bogus" + excinfo.traceback[0].frame.code.path = "bogus" # type: ignore[misc] # noqa: F821 p = FormattedExcinfo(style="short") reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) lines = reprtb.lines @@ -742,7 +761,7 @@ def entry(): assert last_lines[0] == ' raise ValueError("hello")' assert last_lines[1] == "E ValueError: hello" - def test_repr_traceback_and_excinfo(self, importasmod): + def test_repr_traceback_and_excinfo(self, importasmod) -> None: mod = importasmod( """ def f(x): @@ -753,7 +772,8 @@ def entry(): ) excinfo = pytest.raises(ValueError, mod.entry) - for style in ("long", "short"): + styles = ("long", "short") # type: Tuple[_TracebackStyle, ...] + for style in styles: p = FormattedExcinfo(style=style) reprtb = p.repr_traceback(excinfo) assert len(reprtb.reprentries) == 2 @@ -765,10 +785,11 @@ def entry(): assert repr.chain[0][0] assert len(repr.chain[0][0].reprentries) == len(reprtb.reprentries) + assert repr.reprcrash is not None assert repr.reprcrash.path.endswith("mod.py") assert repr.reprcrash.message == "ValueError: 0" - def test_repr_traceback_with_invalid_cwd(self, importasmod, monkeypatch): + def test_repr_traceback_with_invalid_cwd(self, importasmod, monkeypatch) -> None: mod = importasmod( """ def f(x): @@ -787,7 +808,9 @@ def entry(): def raiseos(): nonlocal raised - if sys._getframe().f_back.f_code.co_name == "checked_call": + upframe = sys._getframe().f_back + assert upframe is not None + if upframe.f_code.co_name == "checked_call": # Only raise with expected calls, but not via e.g. inspect for # py38-windows. raised += 1 @@ -831,7 +854,7 @@ def entry(): assert tw_mock.lines[-1] == "content" assert tw_mock.lines[-2] == ("-", "title") - def test_repr_excinfo_reprcrash(self, importasmod): + def test_repr_excinfo_reprcrash(self, importasmod) -> None: mod = importasmod( """ def entry(): @@ -840,6 +863,7 @@ def entry(): ) excinfo = pytest.raises(ValueError, mod.entry) repr = excinfo.getrepr() + assert repr.reprcrash is not None assert repr.reprcrash.path.endswith("mod.py") assert repr.reprcrash.lineno == 3 assert repr.reprcrash.message == "ValueError" @@ -864,7 +888,7 @@ def entry(): assert reprtb.extraline == "!!! Recursion detected (same locals & position)" assert str(reprtb) - def test_reprexcinfo_getrepr(self, importasmod): + def test_reprexcinfo_getrepr(self, importasmod) -> None: mod = importasmod( """ def f(x): @@ -875,14 +899,15 @@ def entry(): ) excinfo = pytest.raises(ValueError, mod.entry) - for style in ("short", "long", "no"): + styles = ("short", "long", "no") # type: Tuple[_TracebackStyle, ...] + for style in styles: for showlocals in (True, False): repr = excinfo.getrepr(style=style, showlocals=showlocals) assert repr.reprtraceback.style == style assert isinstance(repr, ExceptionChainRepr) - for repr in repr.chain: - assert repr[0].style == style + for r in repr.chain: + assert r[0].style == style def test_reprexcinfo_unicode(self): from _pytest._code.code import TerminalRepr diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 93264f3fc61..b5ad94861ae 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -103,7 +103,7 @@ def test_foo(): result.stdout.fnmatch_lines([warning_msg]) -def test_node_direct_ctor_warning(): +def test_node_direct_ctor_warning() -> None: class MockConfig: pass @@ -112,8 +112,8 @@ class MockConfig: DeprecationWarning, match="Direct construction of .* has been deprecated, please use .*.from_parent.*", ) as w: - nodes.Node(name="test", config=ms, session=ms, nodeid="None") - assert w[0].lineno == inspect.currentframe().f_lineno - 1 + nodes.Node(name="test", config=ms, session=ms, nodeid="None") # type: ignore + assert w[0].lineno == inspect.currentframe().f_lineno - 1 # type: ignore assert w[0].filename == __file__ diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_dataclasses.py index 82a685c6314..d96c90a91bd 100644 --- a/testing/example_scripts/dataclasses/test_compare_dataclasses.py +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses.py @@ -2,11 +2,11 @@ from dataclasses import field -def test_dataclasses(): +def test_dataclasses() -> None: @dataclass class SimpleDataObject: field_a: int = field() - field_b: int = field() + field_b: str = field() left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "c") diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py b/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py index fa89e4a2044..7479c66c1be 100644 --- a/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py @@ -2,11 +2,11 @@ from dataclasses import field -def test_dataclasses_with_attribute_comparison_off(): +def test_dataclasses_with_attribute_comparison_off() -> None: @dataclass class SimpleDataObject: field_a: int = field() - field_b: int = field(compare=False) + field_b: str = field(compare=False) left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "c") diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py b/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py index 06634565b16..4737ef904e0 100644 --- a/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py @@ -2,11 +2,11 @@ from dataclasses import field -def test_dataclasses_verbose(): +def test_dataclasses_verbose() -> None: @dataclass class SimpleDataObject: field_a: int = field() - field_b: int = field() + field_b: str = field() left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "c") diff --git a/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py index 4c638e1fcd6..22e981e33f5 100644 --- a/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py +++ b/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py @@ -2,18 +2,18 @@ from dataclasses import field -def test_comparing_two_different_data_classes(): +def test_comparing_two_different_data_classes() -> None: @dataclass class SimpleDataObjectOne: field_a: int = field() - field_b: int = field() + field_b: str = field() @dataclass class SimpleDataObjectTwo: field_a: int = field() - field_b: int = field() + field_b: str = field() left = SimpleDataObjectOne(1, "b") right = SimpleDataObjectTwo(1, "c") - assert left != right + assert left != right # type: ignore[comparison-overlap] # noqa: F821 diff --git a/testing/example_scripts/issue_519.py b/testing/example_scripts/issue_519.py index 7199df820fb..52d5d3f55b1 100644 --- a/testing/example_scripts/issue_519.py +++ b/testing/example_scripts/issue_519.py @@ -1,4 +1,6 @@ import pprint +from typing import List +from typing import Tuple import pytest @@ -13,7 +15,7 @@ def pytest_generate_tests(metafunc): @pytest.fixture(scope="session") def checked_order(): - order = [] + order = [] # type: List[Tuple[str, str, str]] yield order pprint.pprint(order) diff --git a/testing/example_scripts/unittest/test_unittest_asyncio.py b/testing/example_scripts/unittest/test_unittest_asyncio.py index 76eebf74a5e..21b9d2cd963 100644 --- a/testing/example_scripts/unittest/test_unittest_asyncio.py +++ b/testing/example_scripts/unittest/test_unittest_asyncio.py @@ -1,7 +1,8 @@ +from typing import List from unittest import IsolatedAsyncioTestCase # type: ignore -teardowns = [] +teardowns = [] # type: List[None] class AsyncArguments(IsolatedAsyncioTestCase): diff --git a/testing/example_scripts/unittest/test_unittest_asynctest.py b/testing/example_scripts/unittest/test_unittest_asynctest.py index bddbe250a6b..47b5f3f6d63 100644 --- a/testing/example_scripts/unittest/test_unittest_asynctest.py +++ b/testing/example_scripts/unittest/test_unittest_asynctest.py @@ -1,10 +1,11 @@ """Issue #7110""" import asyncio +from typing import List import asynctest -teardowns = [] +teardowns = [] # type: List[None] class Test(asynctest.TestCase): diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index f4ced8facdf..6912a113fa3 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -25,7 +25,7 @@ def __repr__(self): assert s[0] == "(" and s[-1] == ")" -def test_exceptions(): +def test_exceptions() -> None: class BrokenRepr: def __init__(self, ex): self.ex = ex @@ -34,8 +34,8 @@ def __repr__(self): raise self.ex class BrokenReprException(Exception): - __str__ = None - __repr__ = None + __str__ = None # type: ignore[assignment] # noqa: F821 + __repr__ = None # type: ignore[assignment] # noqa: F821 assert "Exception" in saferepr(BrokenRepr(Exception("broken"))) s = saferepr(BrokenReprException("really broken")) @@ -44,7 +44,7 @@ class BrokenReprException(Exception): none = None try: - none() + none() # type: ignore[misc] # noqa: F821 except BaseException as exc: exp_exc = repr(exc) obj = BrokenRepr(BrokenReprException("omg even worse")) @@ -136,10 +136,10 @@ def test_big_repr(): assert len(saferepr(range(1000))) <= len("[" + SafeRepr(0).maxlist * "1000" + "]") -def test_repr_on_newstyle(): +def test_repr_on_newstyle() -> None: class Function: def __repr__(self): - return "<%s>" % (self.name) + return "<%s>" % (self.name) # type: ignore[attr-defined] # noqa: F821 assert saferepr(Function()) diff --git a/testing/logging/test_formatter.py b/testing/logging/test_formatter.py index 85e949d7a78..a90384a9553 100644 --- a/testing/logging/test_formatter.py +++ b/testing/logging/test_formatter.py @@ -1,10 +1,11 @@ import logging +from typing import Any from _pytest._io import TerminalWriter from _pytest.logging import ColoredLevelFormatter -def test_coloredlogformatter(): +def test_coloredlogformatter() -> None: logfmt = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s" record = logging.LogRecord( @@ -14,7 +15,7 @@ def test_coloredlogformatter(): lineno=10, msg="Test Message", args=(), - exc_info=False, + exc_info=None, ) class ColorConfig: @@ -35,7 +36,7 @@ class option: assert output == ("dummypath 10 INFO Test Message") -def test_multiline_message(): +def test_multiline_message() -> None: from _pytest.logging import PercentStyleMultiline logfmt = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s" @@ -47,8 +48,8 @@ def test_multiline_message(): lineno=10, msg="Test Message line1\nline2", args=(), - exc_info=False, - ) + exc_info=None, + ) # type: Any # this is called by logging.Formatter.format record.message = record.getMessage() @@ -124,7 +125,7 @@ def test_multiline_message(): ) -def test_colored_short_level(): +def test_colored_short_level() -> None: logfmt = "%(levelname).1s %(message)s" record = logging.LogRecord( @@ -134,7 +135,7 @@ def test_colored_short_level(): lineno=10, msg="Test Message", args=(), - exc_info=False, + exc_info=None, ) class ColorConfig: diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 709df2b57b0..bbdf28b389a 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -1,9 +1,12 @@ import io import os import re +from typing import cast import pytest +from _pytest.capture import CaptureManager from _pytest.pytester import Testdir +from _pytest.terminal import TerminalReporter def test_nothing_logged(testdir): @@ -808,7 +811,7 @@ def test_log_file(): @pytest.mark.parametrize("has_capture_manager", [True, False]) -def test_live_logging_suspends_capture(has_capture_manager, request): +def test_live_logging_suspends_capture(has_capture_manager: bool, request) -> None: """Test that capture manager is suspended when we emitting messages for live logging. This tests the implementation calls instead of behavior because it is difficult/impossible to do it using @@ -835,8 +838,10 @@ class DummyTerminal(io.StringIO): def section(self, *args, **kwargs): pass - out_file = DummyTerminal() - capture_manager = MockCaptureManager() if has_capture_manager else None + out_file = cast(TerminalReporter, DummyTerminal()) + capture_manager = ( + cast(CaptureManager, MockCaptureManager()) if has_capture_manager else None + ) handler = _LiveLoggingStreamHandler(out_file, capture_manager) handler.set_when("call") @@ -849,7 +854,7 @@ def section(self, *args, **kwargs): assert MockCaptureManager.calls == ["enter disabled", "exit disabled"] else: assert MockCaptureManager.calls == [] - assert out_file.getvalue() == "\nsome message\n" + assert cast(io.StringIO, out_file).getvalue() == "\nsome message\n" def test_collection_live_logging(testdir): diff --git a/testing/python/approx.py b/testing/python/approx.py index 76d995773ea..8581475e1ab 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -428,10 +428,11 @@ def test_numpy_array_wrong_shape(self): assert a12 != approx(a21) assert a21 != approx(a12) - def test_doctests(self, mocked_doctest_runner): + def test_doctests(self, mocked_doctest_runner) -> None: import doctest parser = doctest.DocTestParser() + assert approx.__doc__ is not None test = parser.get_doctest( approx.__doc__, {"approx": approx}, approx.__name__, None, None ) diff --git a/testing/python/collect.py b/testing/python/collect.py index cbc798ad8e0..7824ceff138 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,6 +1,8 @@ import os import sys import textwrap +from typing import Any +from typing import Dict import _pytest._code import pytest @@ -698,7 +700,7 @@ def test_function_with_square_brackets(self, testdir: Testdir) -> None: class TestSorting: - def test_check_equality(self, testdir): + def test_check_equality(self, testdir) -> None: modcol = testdir.getmodulecol( """ def test_pass(): pass @@ -720,10 +722,10 @@ def test_fail(): assert 0 assert fn1 != fn3 for fn in fn1, fn2, fn3: - assert fn != 3 + assert fn != 3 # type: ignore[comparison-overlap] # noqa: F821 assert fn != modcol - assert fn != [1, 2, 3] - assert [1, 2, 3] != fn + assert fn != [1, 2, 3] # type: ignore[comparison-overlap] # noqa: F821 + assert [1, 2, 3] != fn # type: ignore[comparison-overlap] # noqa: F821 assert modcol != fn def test_allow_sane_sorting_for_decorators(self, testdir): @@ -1006,7 +1008,7 @@ def test_failing_fixture(fail_fixture): assert "INTERNALERROR>" not in out result.stdout.fnmatch_lines(["*ValueError: fail me*", "* 1 error in *"]) - def test_filter_traceback_generated_code(self): + def test_filter_traceback_generated_code(self) -> None: """test that filter_traceback() works with the fact that _pytest._code.code.Code.path attribute might return an str object. In this case, one of the entries on the traceback was produced by @@ -1017,17 +1019,18 @@ def test_filter_traceback_generated_code(self): from _pytest.python import filter_traceback try: - ns = {} + ns = {} # type: Dict[str, Any] exec("def foo(): raise ValueError", ns) ns["foo"]() except ValueError: _, _, tb = sys.exc_info() - tb = _pytest._code.Traceback(tb) - assert isinstance(tb[-1].path, str) - assert not filter_traceback(tb[-1]) + assert tb is not None + traceback = _pytest._code.Traceback(tb) + assert isinstance(traceback[-1].path, str) + assert not filter_traceback(traceback[-1]) - def test_filter_traceback_path_no_longer_valid(self, testdir): + def test_filter_traceback_path_no_longer_valid(self, testdir) -> None: """test that filter_traceback() works with the fact that _pytest._code.code.Code.path attribute might return an str object. In this case, one of the files in the traceback no longer exists. @@ -1049,10 +1052,11 @@ def foo(): except ValueError: _, _, tb = sys.exc_info() + assert tb is not None testdir.tmpdir.join("filter_traceback_entry_as_str.py").remove() - tb = _pytest._code.Traceback(tb) - assert isinstance(tb[-1].path, str) - assert filter_traceback(tb[-1]) + traceback = _pytest._code.Traceback(tb) + assert isinstance(traceback[-1].path, str) + assert filter_traceback(traceback[-1]) class TestReportInfo: diff --git a/testing/python/integration.py b/testing/python/integration.py index 3409b644647..537057484d0 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -1,10 +1,14 @@ +from typing import Any + import pytest from _pytest import python from _pytest import runner class TestOEJSKITSpecials: - def test_funcarg_non_pycollectobj(self, testdir, recwarn): # rough jstests usage + def test_funcarg_non_pycollectobj( + self, testdir, recwarn + ) -> None: # rough jstests usage testdir.makeconftest( """ import pytest @@ -28,13 +32,14 @@ class MyClass(object): ) # this hook finds funcarg factories rep = runner.collect_one_node(collector=modcol) - clscol = rep.result[0] + # TODO: Don't treat as Any. + clscol = rep.result[0] # type: Any clscol.obj = lambda arg1: None clscol.funcargs = {} pytest._fillfuncargs(clscol) assert clscol.funcargs["arg1"] == 42 - def test_autouse_fixture(self, testdir, recwarn): # rough jstests usage + def test_autouse_fixture(self, testdir, recwarn) -> None: # rough jstests usage testdir.makeconftest( """ import pytest @@ -61,20 +66,21 @@ class MyClass(object): ) # this hook finds funcarg factories rep = runner.collect_one_node(modcol) - clscol = rep.result[0] + # TODO: Don't treat as Any. + clscol = rep.result[0] # type: Any clscol.obj = lambda: None clscol.funcargs = {} pytest._fillfuncargs(clscol) assert not clscol.funcargs -def test_wrapped_getfslineno(): +def test_wrapped_getfslineno() -> None: def func(): pass def wrap(f): - func.__wrapped__ = f - func.patchings = ["qwe"] + func.__wrapped__ = f # type: ignore + func.patchings = ["qwe"] # type: ignore return func @wrap @@ -87,14 +93,14 @@ def wrapped_func(x, y, z): class TestMockDecoration: - def test_wrapped_getfuncargnames(self): + def test_wrapped_getfuncargnames(self) -> None: from _pytest.compat import getfuncargnames def wrap(f): def func(): pass - func.__wrapped__ = f + func.__wrapped__ = f # type: ignore return func @wrap @@ -322,10 +328,11 @@ def test_fix(fix): ) -def test_pytestconfig_is_session_scoped(): +def test_pytestconfig_is_session_scoped() -> None: from _pytest.fixtures import pytestconfig - assert pytestconfig._pytestfixturefunction.scope == "session" + marker = pytestconfig._pytestfixturefunction # type: ignore + assert marker.scope == "session" class TestNoselikeTestAttribute: diff --git a/testing/python/raises.py b/testing/python/raises.py index 6c607464d54..e55eb6f5472 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -6,9 +6,9 @@ class TestRaises: - def test_check_callable(self): + def test_check_callable(self) -> None: with pytest.raises(TypeError, match=r".* must be callable"): - pytest.raises(RuntimeError, "int('qwe')") + pytest.raises(RuntimeError, "int('qwe')") # type: ignore[call-overload] # noqa: F821 def test_raises(self): excinfo = pytest.raises(ValueError, int, "qwe") @@ -18,19 +18,19 @@ def test_raises_function(self): excinfo = pytest.raises(ValueError, int, "hello") assert "invalid literal" in str(excinfo.value) - def test_raises_callable_no_exception(self): + def test_raises_callable_no_exception(self) -> None: class A: def __call__(self): pass try: pytest.raises(ValueError, A()) - except pytest.raises.Exception: + except pytest.fail.Exception: pass - def test_raises_falsey_type_error(self): + def test_raises_falsey_type_error(self) -> None: with pytest.raises(TypeError): - with pytest.raises(AssertionError, match=0): + with pytest.raises(AssertionError, match=0): # type: ignore[call-overload] # noqa: F821 raise AssertionError("ohai") def test_raises_repr_inflight(self): @@ -126,23 +126,23 @@ def test_division(example_input, expectation): result = testdir.runpytest() result.stdout.fnmatch_lines(["*2 failed*"]) - def test_noclass(self): + def test_noclass(self) -> None: with pytest.raises(TypeError): - pytest.raises("wrong", lambda: None) + pytest.raises("wrong", lambda: None) # type: ignore[call-overload] # noqa: F821 - def test_invalid_arguments_to_raises(self): + def test_invalid_arguments_to_raises(self) -> None: with pytest.raises(TypeError, match="unknown"): - with pytest.raises(TypeError, unknown="bogus"): + with pytest.raises(TypeError, unknown="bogus"): # type: ignore[call-overload] # noqa: F821 raise ValueError() def test_tuple(self): with pytest.raises((KeyError, ValueError)): raise KeyError("oops") - def test_no_raise_message(self): + def test_no_raise_message(self) -> None: try: pytest.raises(ValueError, int, "0") - except pytest.raises.Exception as e: + except pytest.fail.Exception as e: assert e.msg == "DID NOT RAISE {}".format(repr(ValueError)) else: assert False, "Expected pytest.raises.Exception" @@ -150,7 +150,7 @@ def test_no_raise_message(self): try: with pytest.raises(ValueError): pass - except pytest.raises.Exception as e: + except pytest.fail.Exception as e: assert e.msg == "DID NOT RAISE {}".format(repr(ValueError)) else: assert False, "Expected pytest.raises.Exception" @@ -252,7 +252,7 @@ class ClassLooksIterableException(Exception, metaclass=Meta): ): pytest.raises(ClassLooksIterableException, lambda: None) - def test_raises_with_raising_dunder_class(self): + def test_raises_with_raising_dunder_class(self) -> None: """Test current behavior with regard to exceptions via __class__ (#4284).""" class CrappyClass(Exception): @@ -262,12 +262,12 @@ def __class__(self): assert False, "via __class__" with pytest.raises(AssertionError) as excinfo: - with pytest.raises(CrappyClass()): + with pytest.raises(CrappyClass()): # type: ignore[call-overload] # noqa: F821 pass assert "via __class__" in excinfo.value.args[0] def test_raises_context_manager_with_kwargs(self): with pytest.raises(TypeError) as excinfo: - with pytest.raises(Exception, foo="bar"): + with pytest.raises(Exception, foo="bar"): # type: ignore[call-overload] # noqa: F821 pass assert "Unexpected keyword arguments" in str(excinfo.value) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 042aa705502..f28876edcc7 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -279,9 +279,9 @@ def test_other(): ] ) - def test_register_assert_rewrite_checks_types(self): + def test_register_assert_rewrite_checks_types(self) -> None: with pytest.raises(TypeError): - pytest.register_assert_rewrite(["pytest_tests_internal_non_existing"]) + pytest.register_assert_rewrite(["pytest_tests_internal_non_existing"]) # type: ignore pytest.register_assert_rewrite( "pytest_tests_internal_non_existing", "pytest_tests_internal_non_existing2" ) @@ -326,8 +326,10 @@ class TestAssert_reprcompare: def test_different_types(self): assert callequal([0, 1], "foo") is None - def test_summary(self): - summary = callequal([0, 1], [0, 2])[0] + def test_summary(self) -> None: + lines = callequal([0, 1], [0, 2]) + assert lines is not None + summary = lines[0] assert len(summary) < 65 def test_text_diff(self): @@ -337,21 +339,24 @@ def test_text_diff(self): "+ spam", ] - def test_text_skipping(self): + def test_text_skipping(self) -> None: lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs") + assert lines is not None assert "Skipping" in lines[1] for line in lines: assert "a" * 50 not in line - def test_text_skipping_verbose(self): + def test_text_skipping_verbose(self) -> None: lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs", verbose=1) + assert lines is not None assert "- " + "a" * 50 + "eggs" in lines assert "+ " + "a" * 50 + "spam" in lines - def test_multiline_text_diff(self): + def test_multiline_text_diff(self) -> None: left = "foo\nspam\nbar" right = "foo\neggs\nbar" diff = callequal(left, right) + assert diff is not None assert "- eggs" in diff assert "+ spam" in diff @@ -376,8 +381,9 @@ def test_bytes_diff_verbose(self): "+ b'spam'", ] - def test_list(self): + def test_list(self) -> None: expl = callequal([0, 1], [0, 2]) + assert expl is not None assert len(expl) > 1 @pytest.mark.parametrize( @@ -421,21 +427,25 @@ def test_list(self): ), ], ) - def test_iterable_full_diff(self, left, right, expected): + def test_iterable_full_diff(self, left, right, expected) -> None: """Test the full diff assertion failure explanation. When verbose is False, then just a -v notice to get the diff is rendered, when verbose is True, then ndiff of the pprint is returned. """ expl = callequal(left, right, verbose=0) + assert expl is not None assert expl[-1] == "Use -v to get the full diff" - expl = "\n".join(callequal(left, right, verbose=1)) - assert expl.endswith(textwrap.dedent(expected).strip()) + verbose_expl = callequal(left, right, verbose=1) + assert verbose_expl is not None + assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip()) - def test_list_different_lengths(self): + def test_list_different_lengths(self) -> None: expl = callequal([0, 1], [0, 1, 2]) + assert expl is not None assert len(expl) > 1 expl = callequal([0, 1, 2], [0, 1]) + assert expl is not None assert len(expl) > 1 def test_list_wrap_for_multiple_lines(self): @@ -545,27 +555,31 @@ def test_dict_wrap(self): " }", ] - def test_dict(self): + def test_dict(self) -> None: expl = callequal({"a": 0}, {"a": 1}) + assert expl is not None assert len(expl) > 1 - def test_dict_omitting(self): + def test_dict_omitting(self) -> None: lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}) + assert lines is not None assert lines[1].startswith("Omitting 1 identical item") assert "Common items" not in lines for line in lines[1:]: assert "b" not in line - def test_dict_omitting_with_verbosity_1(self): + def test_dict_omitting_with_verbosity_1(self) -> None: """ Ensure differing items are visible for verbosity=1 (#1512) """ lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=1) + assert lines is not None assert lines[1].startswith("Omitting 1 identical item") assert lines[2].startswith("Differing items") assert lines[3] == "{'a': 0} != {'a': 1}" assert "Common items" not in lines - def test_dict_omitting_with_verbosity_2(self): + def test_dict_omitting_with_verbosity_2(self) -> None: lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=2) + assert lines is not None assert lines[1].startswith("Common items:") assert "Omitting" not in lines[1] assert lines[2] == "{'b': 1}" @@ -614,15 +628,17 @@ def test_sequence_different_items(self): "+ (1, 2, 3)", ] - def test_set(self): + def test_set(self) -> None: expl = callequal({0, 1}, {0, 2}) + assert expl is not None assert len(expl) > 1 - def test_frozenzet(self): + def test_frozenzet(self) -> None: expl = callequal(frozenset([0, 1]), {0, 2}) + assert expl is not None assert len(expl) > 1 - def test_Sequence(self): + def test_Sequence(self) -> None: # Test comparing with a Sequence subclass. class TestSequence(collections.abc.MutableSequence): def __init__(self, iterable): @@ -644,15 +660,18 @@ def insert(self, item, index): pass expl = callequal(TestSequence([0, 1]), list([0, 2])) + assert expl is not None assert len(expl) > 1 - def test_list_tuples(self): + def test_list_tuples(self) -> None: expl = callequal([], [(1, 2)]) + assert expl is not None assert len(expl) > 1 expl = callequal([(1, 2)], []) + assert expl is not None assert len(expl) > 1 - def test_repr_verbose(self): + def test_repr_verbose(self) -> None: class Nums: def __init__(self, nums): self.nums = nums @@ -669,21 +688,25 @@ def __repr__(self): assert callequal(nums_x, nums_y) is None expl = callequal(nums_x, nums_y, verbose=1) + assert expl is not None assert "+" + repr(nums_x) in expl assert "-" + repr(nums_y) in expl expl = callequal(nums_x, nums_y, verbose=2) + assert expl is not None assert "+" + repr(nums_x) in expl assert "-" + repr(nums_y) in expl - def test_list_bad_repr(self): + def test_list_bad_repr(self) -> None: class A: def __repr__(self): raise ValueError(42) expl = callequal([], [A()]) + assert expl is not None assert "ValueError" in "".join(expl) expl = callequal({}, {"1": A()}, verbose=2) + assert expl is not None assert expl[0].startswith("{} == <[ValueError") assert "raised in repr" in expl[0] assert expl[1:] == [ @@ -707,9 +730,10 @@ def __repr__(self): expl = callequal(A(), "") assert not expl - def test_repr_no_exc(self): - expl = " ".join(callequal("foo", "bar")) - assert "raised in repr()" not in expl + def test_repr_no_exc(self) -> None: + expl = callequal("foo", "bar") + assert expl is not None + assert "raised in repr()" not in " ".join(expl) def test_unicode(self): assert callequal("£€", "£") == [ @@ -734,11 +758,12 @@ def __repr__(self): def test_format_nonascii_explanation(self): assert util.format_explanation("λ") - def test_mojibake(self): + def test_mojibake(self) -> None: # issue 429 left = b"e" right = b"\xc3\xa9" expl = callequal(left, right) + assert expl is not None for line in expl: assert isinstance(line, str) msg = "\n".join(expl) @@ -791,7 +816,7 @@ def test_comparing_two_different_data_classes(self, testdir): class TestAssert_reprcompare_attrsclass: - def test_attrs(self): + def test_attrs(self) -> None: @attr.s class SimpleDataObject: field_a = attr.ib() @@ -801,12 +826,13 @@ class SimpleDataObject: right = SimpleDataObject(1, "c") lines = callequal(left, right) + assert lines is not None assert lines[1].startswith("Omitting 1 identical item") assert "Matching attributes" not in lines for line in lines[1:]: assert "field_a" not in line - def test_attrs_verbose(self): + def test_attrs_verbose(self) -> None: @attr.s class SimpleDataObject: field_a = attr.ib() @@ -816,6 +842,7 @@ class SimpleDataObject: right = SimpleDataObject(1, "c") lines = callequal(left, right, verbose=2) + assert lines is not None assert lines[1].startswith("Matching attributes:") assert "Omitting" not in lines[1] assert lines[2] == "['field_a']" @@ -824,12 +851,13 @@ def test_attrs_with_attribute_comparison_off(self): @attr.s class SimpleDataObject: field_a = attr.ib() - field_b = attr.ib(**{ATTRS_EQ_FIELD: False}) + field_b = attr.ib(**{ATTRS_EQ_FIELD: False}) # type: ignore left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "b") lines = callequal(left, right, verbose=2) + assert lines is not None assert lines[1].startswith("Matching attributes:") assert "Omitting" not in lines[1] assert lines[2] == "['field_a']" @@ -946,8 +974,8 @@ class TestTruncateExplanation: # to calculate that results have the expected length. LINES_IN_TRUNCATION_MSG = 2 - def test_doesnt_truncate_when_input_is_empty_list(self): - expl = [] + def test_doesnt_truncate_when_input_is_empty_list(self) -> None: + expl = [] # type: List[str] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result == expl diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 212c631ef59..3813993bec1 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -9,6 +9,13 @@ import textwrap import zipfile from functools import partial +from typing import Dict +from typing import List +from typing import Mapping +from typing import Optional +from typing import Set + +import py import _pytest._code import pytest @@ -25,24 +32,26 @@ from _pytest.pytester import Testdir -def rewrite(src): +def rewrite(src: str) -> ast.Module: tree = ast.parse(src) rewrite_asserts(tree, src.encode()) return tree -def getmsg(f, extra_ns=None, must_pass=False): +def getmsg( + f, extra_ns: Optional[Mapping[str, object]] = None, *, must_pass: bool = False +) -> Optional[str]: """Rewrite the assertions in f, run it, and get the failure message.""" src = "\n".join(_pytest._code.Code(f).source().lines) mod = rewrite(src) code = compile(mod, "", "exec") - ns = {} + ns = {} # type: Dict[str, object] if extra_ns is not None: ns.update(extra_ns) exec(code, ns) func = ns[f.__name__] try: - func() + func() # type: ignore[operator] # noqa: F821 except AssertionError: if must_pass: pytest.fail("shouldn't have raised") @@ -53,6 +62,7 @@ def getmsg(f, extra_ns=None, must_pass=False): else: if not must_pass: pytest.fail("function didn't raise at all") + return None class TestAssertionRewrite: @@ -98,10 +108,11 @@ def test_place_initial_imports(self): assert imp.col_offset == 0 assert isinstance(m.body[3], ast.Expr) - def test_dont_rewrite(self): + def test_dont_rewrite(self) -> None: s = """'PYTEST_DONT_REWRITE'\nassert 14""" m = rewrite(s) assert len(m.body) == 2 + assert isinstance(m.body[1], ast.Assert) assert m.body[1].msg is None def test_dont_rewrite_plugin(self, testdir): @@ -145,28 +156,28 @@ def test_honors_pep_235(self, testdir, monkeypatch): monkeypatch.syspath_prepend(xdir) testdir.runpytest().assert_outcomes(passed=1) - def test_name(self, request): - def f(): + def test_name(self, request) -> None: + def f1() -> None: assert False - assert getmsg(f) == "assert False" + assert getmsg(f1) == "assert False" - def f(): + def f2() -> None: f = False assert f - assert getmsg(f) == "assert False" + assert getmsg(f2) == "assert False" - def f(): - assert a_global # noqa + def f3() -> None: + assert a_global # type: ignore[name-defined] # noqa - assert getmsg(f, {"a_global": False}) == "assert False" + assert getmsg(f3, {"a_global": False}) == "assert False" - def f(): - assert sys == 42 + def f4() -> None: + assert sys == 42 # type: ignore[comparison-overlap] # noqa: F821 verbose = request.config.getoption("verbose") - msg = getmsg(f, {"sys": sys}) + msg = getmsg(f4, {"sys": sys}) if verbose > 0: assert msg == ( "assert == 42\n" @@ -176,64 +187,74 @@ def f(): else: assert msg == "assert sys == 42" - def f(): - assert cls == 42 # noqa: F821 + def f5() -> None: + assert cls == 42 # type: ignore[name-defined] # noqa: F821 class X: pass - msg = getmsg(f, {"cls": X}).splitlines() + msg = getmsg(f5, {"cls": X}) + assert msg is not None + lines = msg.splitlines() if verbose > 1: - assert msg == ["assert {!r} == 42".format(X), " +{!r}".format(X), " -42"] + assert lines == [ + "assert {!r} == 42".format(X), + " +{!r}".format(X), + " -42", + ] elif verbose > 0: - assert msg == [ + assert lines == [ "assert .X'> == 42", " +{!r}".format(X), " -42", ] else: - assert msg == ["assert cls == 42"] + assert lines == ["assert cls == 42"] - def test_assertrepr_compare_same_width(self, request): + def test_assertrepr_compare_same_width(self, request) -> None: """Should use same width/truncation with same initial width.""" - def f(): + def f() -> None: assert "1234567890" * 5 + "A" == "1234567890" * 5 + "B" - msg = getmsg(f).splitlines()[0] + msg = getmsg(f) + assert msg is not None + line = msg.splitlines()[0] if request.config.getoption("verbose") > 1: - assert msg == ( + assert line == ( "assert '12345678901234567890123456789012345678901234567890A' " "== '12345678901234567890123456789012345678901234567890B'" ) else: - assert msg == ( + assert line == ( "assert '123456789012...901234567890A' " "== '123456789012...901234567890B'" ) - def test_dont_rewrite_if_hasattr_fails(self, request): + def test_dont_rewrite_if_hasattr_fails(self, request) -> None: class Y: """ A class whos getattr fails, but not with `AttributeError` """ def __getattr__(self, attribute_name): raise KeyError() - def __repr__(self): + def __repr__(self) -> str: return "Y" - def __init__(self): + def __init__(self) -> None: self.foo = 3 - def f(): - assert cls().foo == 2 # noqa + def f() -> None: + assert cls().foo == 2 # type: ignore[name-defined] # noqa: F821 # XXX: looks like the "where" should also be there in verbose mode?! - message = getmsg(f, {"cls": Y}).splitlines() + msg = getmsg(f, {"cls": Y}) + assert msg is not None + lines = msg.splitlines() if request.config.getoption("verbose") > 0: - assert message == ["assert 3 == 2", " +3", " -2"] + assert lines == ["assert 3 == 2", " +3", " -2"] else: - assert message == [ + assert lines == [ "assert 3 == 2", " + where 3 = Y.foo", " + where Y = cls()", @@ -314,145 +335,145 @@ def test_assertion_messages_bytes(self, testdir): assert result.ret == 1 result.stdout.fnmatch_lines(["*AssertionError: b'ohai!'", "*assert False"]) - def test_boolop(self): - def f(): + def test_boolop(self) -> None: + def f1() -> None: f = g = False assert f and g - assert getmsg(f) == "assert (False)" + assert getmsg(f1) == "assert (False)" - def f(): + def f2() -> None: f = True g = False assert f and g - assert getmsg(f) == "assert (True and False)" + assert getmsg(f2) == "assert (True and False)" - def f(): + def f3() -> None: f = False g = True assert f and g - assert getmsg(f) == "assert (False)" + assert getmsg(f3) == "assert (False)" - def f(): + def f4() -> None: f = g = False assert f or g - assert getmsg(f) == "assert (False or False)" + assert getmsg(f4) == "assert (False or False)" - def f(): + def f5() -> None: f = g = False assert not f and not g - getmsg(f, must_pass=True) + getmsg(f5, must_pass=True) - def x(): + def x() -> bool: return False - def f(): + def f6() -> None: assert x() and x() assert ( - getmsg(f, {"x": x}) + getmsg(f6, {"x": x}) == """assert (False) + where False = x()""" ) - def f(): + def f7() -> None: assert False or x() assert ( - getmsg(f, {"x": x}) + getmsg(f7, {"x": x}) == """assert (False or False) + where False = x()""" ) - def f(): + def f8() -> None: assert 1 in {} and 2 in {} - assert getmsg(f) == "assert (1 in {})" + assert getmsg(f8) == "assert (1 in {})" - def f(): + def f9() -> None: x = 1 y = 2 assert x in {1: None} and y in {} - assert getmsg(f) == "assert (1 in {1: None} and 2 in {})" + assert getmsg(f9) == "assert (1 in {1: None} and 2 in {})" - def f(): + def f10() -> None: f = True g = False assert f or g - getmsg(f, must_pass=True) + getmsg(f10, must_pass=True) - def f(): + def f11() -> None: f = g = h = lambda: True assert f() and g() and h() - getmsg(f, must_pass=True) + getmsg(f11, must_pass=True) - def test_short_circuit_evaluation(self): - def f(): - assert True or explode # noqa + def test_short_circuit_evaluation(self) -> None: + def f1() -> None: + assert True or explode # type: ignore[name-defined] # noqa: F821 - getmsg(f, must_pass=True) + getmsg(f1, must_pass=True) - def f(): + def f2() -> None: x = 1 assert x == 1 or x == 2 - getmsg(f, must_pass=True) + getmsg(f2, must_pass=True) - def test_unary_op(self): - def f(): + def test_unary_op(self) -> None: + def f1() -> None: x = True assert not x - assert getmsg(f) == "assert not True" + assert getmsg(f1) == "assert not True" - def f(): + def f2() -> None: x = 0 assert ~x + 1 - assert getmsg(f) == "assert (~0 + 1)" + assert getmsg(f2) == "assert (~0 + 1)" - def f(): + def f3() -> None: x = 3 assert -x + x - assert getmsg(f) == "assert (-3 + 3)" + assert getmsg(f3) == "assert (-3 + 3)" - def f(): + def f4() -> None: x = 0 assert +x + x - assert getmsg(f) == "assert (+0 + 0)" + assert getmsg(f4) == "assert (+0 + 0)" - def test_binary_op(self): - def f(): + def test_binary_op(self) -> None: + def f1() -> None: x = 1 y = -1 assert x + y - assert getmsg(f) == "assert (1 + -1)" + assert getmsg(f1) == "assert (1 + -1)" - def f(): + def f2() -> None: assert not 5 % 4 - assert getmsg(f) == "assert not (5 % 4)" + assert getmsg(f2) == "assert not (5 % 4)" - def test_boolop_percent(self): - def f(): + def test_boolop_percent(self) -> None: + def f1() -> None: assert 3 % 2 and False - assert getmsg(f) == "assert ((3 % 2) and False)" + assert getmsg(f1) == "assert ((3 % 2) and False)" - def f(): + def f2() -> None: assert False or 4 % 2 - assert getmsg(f) == "assert (False or (4 % 2))" + assert getmsg(f2) == "assert (False or (4 % 2))" def test_at_operator_issue1290(self, testdir): testdir.makepyfile( @@ -480,133 +501,133 @@ def test(): ) testdir.runpytest().assert_outcomes(passed=1) - def test_call(self): - def g(a=42, *args, **kwargs): + def test_call(self) -> None: + def g(a=42, *args, **kwargs) -> bool: return False ns = {"g": g} - def f(): + def f1() -> None: assert g() assert ( - getmsg(f, ns) + getmsg(f1, ns) == """assert False + where False = g()""" ) - def f(): + def f2() -> None: assert g(1) assert ( - getmsg(f, ns) + getmsg(f2, ns) == """assert False + where False = g(1)""" ) - def f(): + def f3() -> None: assert g(1, 2) assert ( - getmsg(f, ns) + getmsg(f3, ns) == """assert False + where False = g(1, 2)""" ) - def f(): + def f4() -> None: assert g(1, g=42) assert ( - getmsg(f, ns) + getmsg(f4, ns) == """assert False + where False = g(1, g=42)""" ) - def f(): + def f5() -> None: assert g(1, 3, g=23) assert ( - getmsg(f, ns) + getmsg(f5, ns) == """assert False + where False = g(1, 3, g=23)""" ) - def f(): + def f6() -> None: seq = [1, 2, 3] assert g(*seq) assert ( - getmsg(f, ns) + getmsg(f6, ns) == """assert False + where False = g(*[1, 2, 3])""" ) - def f(): + def f7() -> None: x = "a" assert g(**{x: 2}) assert ( - getmsg(f, ns) + getmsg(f7, ns) == """assert False + where False = g(**{'a': 2})""" ) - def test_attribute(self): + def test_attribute(self) -> None: class X: g = 3 ns = {"x": X} - def f(): - assert not x.g # noqa + def f1() -> None: + assert not x.g # type: ignore[name-defined] # noqa: F821 assert ( - getmsg(f, ns) + getmsg(f1, ns) == """assert not 3 + where 3 = x.g""" ) - def f(): - x.a = False # noqa - assert x.a # noqa + def f2() -> None: + x.a = False # type: ignore[name-defined] # noqa: F821 + assert x.a # type: ignore[name-defined] # noqa: F821 assert ( - getmsg(f, ns) + getmsg(f2, ns) == """assert False + where False = x.a""" ) - def test_comparisons(self): - def f(): + def test_comparisons(self) -> None: + def f1() -> None: a, b = range(2) assert b < a - assert getmsg(f) == """assert 1 < 0""" + assert getmsg(f1) == """assert 1 < 0""" - def f(): + def f2() -> None: a, b, c = range(3) assert a > b > c - assert getmsg(f) == """assert 0 > 1""" + assert getmsg(f2) == """assert 0 > 1""" - def f(): + def f3() -> None: a, b, c = range(3) assert a < b > c - assert getmsg(f) == """assert 1 > 2""" + assert getmsg(f3) == """assert 1 > 2""" - def f(): + def f4() -> None: a, b, c = range(3) assert a < b <= c - getmsg(f, must_pass=True) + getmsg(f4, must_pass=True) - def f(): + def f5() -> None: a, b, c = range(3) assert a < b assert b < c - getmsg(f, must_pass=True) + getmsg(f5, must_pass=True) def test_len(self, request): def f(): @@ -619,29 +640,29 @@ def f(): else: assert msg == "assert 10 == 11\n + where 10 = len([0, 1, 2, 3, 4, 5, ...])" - def test_custom_reprcompare(self, monkeypatch): - def my_reprcompare(op, left, right): + def test_custom_reprcompare(self, monkeypatch) -> None: + def my_reprcompare1(op, left, right) -> str: return "42" - monkeypatch.setattr(util, "_reprcompare", my_reprcompare) + monkeypatch.setattr(util, "_reprcompare", my_reprcompare1) - def f(): + def f1() -> None: assert 42 < 3 - assert getmsg(f) == "assert 42" + assert getmsg(f1) == "assert 42" - def my_reprcompare(op, left, right): + def my_reprcompare2(op, left, right) -> str: return "{} {} {}".format(left, op, right) - monkeypatch.setattr(util, "_reprcompare", my_reprcompare) + monkeypatch.setattr(util, "_reprcompare", my_reprcompare2) - def f(): + def f2() -> None: assert 1 < 3 < 5 <= 4 < 7 - assert getmsg(f) == "assert 5 <= 4" + assert getmsg(f2) == "assert 5 <= 4" - def test_assert_raising__bool__in_comparison(self): - def f(): + def test_assert_raising__bool__in_comparison(self) -> None: + def f() -> None: class A: def __bool__(self): raise ValueError(42) @@ -652,21 +673,25 @@ def __lt__(self, other): def __repr__(self): return "" - def myany(x): + def myany(x) -> bool: return False assert myany(A() < 0) - assert " < 0" in getmsg(f) + msg = getmsg(f) + assert msg is not None + assert " < 0" in msg - def test_formatchar(self): - def f(): - assert "%test" == "test" + def test_formatchar(self) -> None: + def f() -> None: + assert "%test" == "test" # type: ignore[comparison-overlap] # noqa: F821 - assert getmsg(f).startswith("assert '%test' == 'test'") + msg = getmsg(f) + assert msg is not None + assert msg.startswith("assert '%test' == 'test'") - def test_custom_repr(self, request): - def f(): + def test_custom_repr(self, request) -> None: + def f() -> None: class Foo: a = 1 @@ -676,14 +701,16 @@ def __repr__(self): f = Foo() assert 0 == f.a - lines = util._format_lines([getmsg(f)]) + msg = getmsg(f) + assert msg is not None + lines = util._format_lines([msg]) if request.config.getoption("verbose") > 0: assert lines == ["assert 0 == 1\n +0\n -1"] else: assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"] - def test_custom_repr_non_ascii(self): - def f(): + def test_custom_repr_non_ascii(self) -> None: + def f() -> None: class A: name = "ä" @@ -694,6 +721,7 @@ def __repr__(self): assert not a.name msg = getmsg(f) + assert msg is not None assert "UnicodeDecodeError" not in msg assert "UnicodeEncodeError" not in msg @@ -895,6 +923,7 @@ def test_remember_rewritten_modules(self, pytestconfig, testdir, monkeypatch): hook, "_warn_already_imported", lambda code, msg: warnings.append(msg) ) spec = hook.find_spec("test_remember_rewritten_modules") + assert spec is not None module = importlib.util.module_from_spec(spec) hook.exec_module(module) hook.mark_rewrite("test_remember_rewritten_modules") @@ -1007,7 +1036,7 @@ def test_load_resource(): result = testdir.runpytest_subprocess() result.assert_outcomes(passed=1) - def test_read_pyc(self, tmpdir): + def test_read_pyc(self, tmp_path: Path) -> None: """ Ensure that the `_read_pyc` can properly deal with corrupted pyc files. In those circumstances it should just give up instead of generating @@ -1016,18 +1045,18 @@ def test_read_pyc(self, tmpdir): import py_compile from _pytest.assertion.rewrite import _read_pyc - source = tmpdir.join("source.py") - pyc = source + "c" + source = tmp_path / "source.py" + pyc = Path(str(source) + "c") - source.write("def test(): pass") + source.write_text("def test(): pass") py_compile.compile(str(source), str(pyc)) - contents = pyc.read(mode="rb") + contents = pyc.read_bytes() strip_bytes = 20 # header is around 8 bytes, strip a little more assert len(contents) > strip_bytes - pyc.write(contents[:strip_bytes], mode="wb") + pyc.write_bytes(contents[:strip_bytes]) - assert _read_pyc(str(source), str(pyc)) is None # no error + assert _read_pyc(source, pyc) is None # no error def test_reload_is_same_and_reloads(self, testdir: Testdir) -> None: """Reloading a (collected) module after change picks up the change.""" @@ -1178,17 +1207,17 @@ def test(): pass assert result.ret == 0 -def test_rewrite_infinite_recursion(testdir, pytestconfig, monkeypatch): +def test_rewrite_infinite_recursion(testdir, pytestconfig, monkeypatch) -> None: """Fix infinite recursion when writing pyc files: if an import happens to be triggered when writing the pyc file, this would cause another call to the hook, which would trigger another pyc writing, which could trigger another import, and so on. (#3506)""" - from _pytest.assertion import rewrite + from _pytest.assertion import rewrite as rewritemod testdir.syspathinsert() testdir.makepyfile(test_foo="def test_foo(): pass") testdir.makepyfile(test_bar="def test_bar(): pass") - original_write_pyc = rewrite._write_pyc + original_write_pyc = rewritemod._write_pyc write_pyc_called = [] @@ -1199,7 +1228,7 @@ def spy_write_pyc(*args, **kwargs): assert hook.find_spec("test_bar") is None return original_write_pyc(*args, **kwargs) - monkeypatch.setattr(rewrite, "_write_pyc", spy_write_pyc) + monkeypatch.setattr(rewritemod, "_write_pyc", spy_write_pyc) monkeypatch.setattr(sys, "dont_write_bytecode", False) hook = AssertionRewritingHook(pytestconfig) @@ -1212,14 +1241,14 @@ def spy_write_pyc(*args, **kwargs): class TestEarlyRewriteBailout: @pytest.fixture - def hook(self, pytestconfig, monkeypatch, testdir): + def hook(self, pytestconfig, monkeypatch, testdir) -> AssertionRewritingHook: """Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track if PathFinder.find_spec has been called. """ import importlib.machinery - self.find_spec_calls = [] - self.initial_paths = set() + self.find_spec_calls = [] # type: List[str] + self.initial_paths = set() # type: Set[py.path.local] class StubSession: _initialpaths = self.initial_paths @@ -1229,17 +1258,17 @@ def isinitpath(self, p): def spy_find_spec(name, path): self.find_spec_calls.append(name) - return importlib.machinery.PathFinder.find_spec(name, path) + return importlib.machinery.PathFinder.find_spec(name, path) # type: ignore hook = AssertionRewritingHook(pytestconfig) # use default patterns, otherwise we inherit pytest's testing config hook.fnpats[:] = ["test_*.py", "*_test.py"] monkeypatch.setattr(hook, "_find_spec", spy_find_spec) - hook.set_session(StubSession()) + hook.set_session(StubSession()) # type: ignore[arg-type] # noqa: F821 testdir.syspathinsert() return hook - def test_basic(self, testdir, hook): + def test_basic(self, testdir, hook: AssertionRewritingHook) -> None: """ Ensure we avoid calling PathFinder.find_spec when we know for sure a certain module will not be rewritten to optimize assertion rewriting (#3918). @@ -1272,7 +1301,9 @@ def fix(): return 1 assert hook.find_spec("foobar") is not None assert self.find_spec_calls == ["conftest", "test_foo", "foobar"] - def test_pattern_contains_subdirectories(self, testdir, hook): + def test_pattern_contains_subdirectories( + self, testdir, hook: AssertionRewritingHook + ) -> None: """If one of the python_files patterns contain subdirectories ("tests/**.py") we can't bailout early because we need to match with the full path, which can only be found by calling PathFinder.find_spec """ @@ -1515,17 +1546,17 @@ def test_get_assertion_exprs(src, expected): assert _get_assertion_exprs(src) == expected -def test_try_makedirs(monkeypatch, tmp_path): +def test_try_makedirs(monkeypatch, tmp_path: Path) -> None: from _pytest.assertion.rewrite import try_makedirs p = tmp_path / "foo" # create - assert try_makedirs(str(p)) + assert try_makedirs(p) assert p.is_dir() # already exist - assert try_makedirs(str(p)) + assert try_makedirs(p) # monkeypatch to simulate all error situations def fake_mkdir(p, exist_ok=False, *, exc): @@ -1533,25 +1564,25 @@ def fake_mkdir(p, exist_ok=False, *, exc): raise exc monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=FileNotFoundError())) - assert not try_makedirs(str(p)) + assert not try_makedirs(p) monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=NotADirectoryError())) - assert not try_makedirs(str(p)) + assert not try_makedirs(p) monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=PermissionError())) - assert not try_makedirs(str(p)) + assert not try_makedirs(p) err = OSError() err.errno = errno.EROFS monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err)) - assert not try_makedirs(str(p)) + assert not try_makedirs(p) # unhandled OSError should raise err = OSError() err.errno = errno.ECHILD monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err)) with pytest.raises(OSError) as exc_info: - try_makedirs(str(p)) + try_makedirs(p) assert exc_info.value.errno == errno.ECHILD diff --git a/testing/test_capture.py b/testing/test_capture.py index 1301a0e69d3..9e5036a6682 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -6,7 +6,9 @@ import textwrap from io import UnsupportedOperation from typing import BinaryIO +from typing import cast from typing import Generator +from typing import TextIO import pytest from _pytest import capture @@ -1351,7 +1353,7 @@ def test_capattr(): not sys.platform.startswith("win") and sys.version_info[:2] >= (3, 6), reason="only py3.6+ on windows", ) -def test_py36_windowsconsoleio_workaround_non_standard_streams(): +def test_py36_windowsconsoleio_workaround_non_standard_streams() -> None: """ Ensure _py36_windowsconsoleio_workaround function works with objects that do not implement the full ``io``-based stream protocol, for example execnet channels (#2666). @@ -1362,7 +1364,7 @@ class DummyStream: def write(self, s): pass - stream = DummyStream() + stream = cast(TextIO, DummyStream()) _py36_windowsconsoleio_workaround(stream) diff --git a/testing/test_collection.py b/testing/test_collection.py index dfbfe9ba818..8e5d5aaccb0 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -634,13 +634,14 @@ def test_method(self): class Test_getinitialnodes: - def test_global_file(self, testdir, tmpdir): + def test_global_file(self, testdir, tmpdir) -> None: x = tmpdir.ensure("x.py") with tmpdir.as_cwd(): config = testdir.parseconfigure(x) col = testdir.getnode(config, x) assert isinstance(col, pytest.Module) assert col.name == "x.py" + assert col.parent is not None assert col.parent.parent is None for col in col.listchain(): assert col.config is config diff --git a/testing/test_config.py b/testing/test_config.py index c102202edd8..867012e932c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -2,6 +2,9 @@ import re import sys import textwrap +from typing import Dict +from typing import List +from typing import Sequence import py.path @@ -264,9 +267,9 @@ def test_absolute_win32_path(self, testdir): class TestConfigAPI: - def test_config_trace(self, testdir): + def test_config_trace(self, testdir) -> None: config = testdir.parseconfig() - values = [] + values = [] # type: List[str] config.trace.root.setwriter(values.append) config.trace("hello") assert len(values) == 1 @@ -519,9 +522,9 @@ def test_basic_behavior(self, _sys_snapshot): assert config.option.capture == "no" assert config.args == args - def test_invocation_params_args(self, _sys_snapshot): + def test_invocation_params_args(self, _sys_snapshot) -> None: """Show that fromdictargs can handle args in their "orig" format""" - option_dict = {} + option_dict = {} # type: Dict[str, object] args = ["-vvvv", "-s", "a", "b"] config = Config.fromdictargs(option_dict, args) @@ -566,8 +569,8 @@ def test_inifilename(self, tmpdir): assert config.inicfg.get("should_not_be_set") is None -def test_options_on_small_file_do_not_blow_up(testdir): - def runfiletest(opts): +def test_options_on_small_file_do_not_blow_up(testdir) -> None: + def runfiletest(opts: Sequence[str]) -> None: reprec = testdir.inline_run(*opts) passed, skipped, failed = reprec.countoutcomes() assert failed == 2 @@ -580,19 +583,16 @@ def test_f2(): assert 0 """ ) - for opts in ( - [], - ["-l"], - ["-s"], - ["--tb=no"], - ["--tb=short"], - ["--tb=long"], - ["--fulltrace"], - ["--traceconfig"], - ["-v"], - ["-v", "-v"], - ): - runfiletest(opts + [path]) + runfiletest([path]) + runfiletest(["-l", path]) + runfiletest(["-s", path]) + runfiletest(["--tb=no", path]) + runfiletest(["--tb=short", path]) + runfiletest(["--tb=long", path]) + runfiletest(["--fulltrace", path]) + runfiletest(["--traceconfig", path]) + runfiletest(["-v", path]) + runfiletest(["-v", "-v", path]) def test_preparse_ordering_with_setuptools(testdir, monkeypatch): @@ -1360,7 +1360,7 @@ class DummyPlugin: # args cannot be None with pytest.raises(TypeError): - Config.InvocationParams(args=None, plugins=None, dir=Path()) + Config.InvocationParams(args=None, plugins=None, dir=Path()) # type: ignore[arg-type] # noqa: F821 @pytest.mark.parametrize( diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 00af4a088a7..948b621f7c7 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -50,7 +50,7 @@ def reset(self): def interaction(self, *args): called.append("interaction") - _pytest._CustomPdb = _CustomPdb + _pytest._CustomPdb = _CustomPdb # type: ignore return called @@ -73,9 +73,9 @@ def set_trace(self, frame): print("**CustomDebugger**") called.append("set_trace") - _pytest._CustomDebugger = _CustomDebugger + _pytest._CustomDebugger = _CustomDebugger # type: ignore yield called - del _pytest._CustomDebugger + del _pytest._CustomDebugger # type: ignore class TestPDB: @@ -895,7 +895,7 @@ def test_supports_breakpoint_module_global(self): if sys.version_info >= (3, 7): assert SUPPORTS_BREAKPOINT_BUILTIN is True if sys.version_info.major == 3 and sys.version_info.minor == 5: - assert SUPPORTS_BREAKPOINT_BUILTIN is False + assert SUPPORTS_BREAKPOINT_BUILTIN is False # type: ignore[comparison-overlap] @pytest.mark.skipif( not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin" diff --git a/testing/test_doctest.py b/testing/test_doctest.py index c3ba60deb04..2b98b5267da 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,5 +1,7 @@ import inspect import textwrap +from typing import Callable +from typing import Optional import pytest from _pytest.compat import MODULE_NOT_FOUND_ERROR @@ -1477,7 +1479,9 @@ def __getattr__(self, _): @pytest.mark.parametrize( # pragma: no branch (lambdas are not called) "stop", [None, _is_mocked, lambda f: None, lambda f: False, lambda f: True] ) -def test_warning_on_unwrap_of_broken_object(stop): +def test_warning_on_unwrap_of_broken_object( + stop: Optional[Callable[[object], object]] +) -> None: bad_instance = Broken() assert inspect.unwrap.__module__ == "inspect" with _patch_unwrap_mock_aware(): @@ -1486,7 +1490,7 @@ def test_warning_on_unwrap_of_broken_object(stop): pytest.PytestWarning, match="^Got KeyError.* when unwrapping" ): with pytest.raises(KeyError): - inspect.unwrap(bad_instance, stop=stop) + inspect.unwrap(bad_instance, stop=stop) # type: ignore[arg-type] # noqa: F821 assert inspect.unwrap.__module__ == "inspect" diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 83e61e1d9de..d7771cc9708 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,16 +1,22 @@ import os import platform from datetime import datetime +from typing import cast +from typing import List +from typing import Tuple from xml.dom import minidom import py import xmlschema import pytest +from _pytest.compat import TYPE_CHECKING +from _pytest.config import Config from _pytest.junitxml import bin_xml_escape from _pytest.junitxml import LogXML from _pytest.pathlib import Path from _pytest.reports import BaseReport +from _pytest.reports import TestReport from _pytest.store import Store @@ -860,10 +866,13 @@ def test_mangle_test_address(): assert newnames == ["a.my.py.thing", "Class", "method", "[a-1-::]"] -def test_dont_configure_on_slaves(tmpdir): - gotten = [] +def test_dont_configure_on_slaves(tmpdir) -> None: + gotten = [] # type: List[object] class FakeConfig: + if TYPE_CHECKING: + slaveinput = None + def __init__(self): self.pluginmanager = self self.option = self @@ -877,7 +886,7 @@ def getini(self, name): xmlpath = str(tmpdir.join("junix.xml")) register = gotten.append - fake_config = FakeConfig() + fake_config = cast(Config, FakeConfig()) from _pytest import junitxml junitxml.pytest_configure(fake_config) @@ -1089,18 +1098,18 @@ def test_func(self, param): node.assert_attr(name="test_func[double::colon]") -def test_unicode_issue368(testdir): +def test_unicode_issue368(testdir) -> None: path = testdir.tmpdir.join("test.xml") log = LogXML(str(path), None) ustr = "ВНИ!" class Report(BaseReport): longrepr = ustr - sections = [] + sections = [] # type: List[Tuple[str, str]] nodeid = "something" location = "tests/filename.py", 42, "TestClass.method" - test_report = Report() + test_report = cast(TestReport, Report()) # hopefully this is not too brittle ... log.pytest_sessionstart() @@ -1113,7 +1122,7 @@ class Report(BaseReport): node_reporter.append_skipped(test_report) test_report.longrepr = "filename", 1, "Skipped: 卡嘣嘣" node_reporter.append_skipped(test_report) - test_report.wasxfail = ustr + test_report.wasxfail = ustr # type: ignore[attr-defined] # noqa: F821 node_reporter.append_skipped(test_report) log.pytest_sessionfinish() @@ -1363,17 +1372,17 @@ def test_pass(): @parametrize_families -def test_global_properties(testdir, xunit_family): +def test_global_properties(testdir, xunit_family) -> None: path = testdir.tmpdir.join("test_global_properties.xml") log = LogXML(str(path), None, family=xunit_family) class Report(BaseReport): - sections = [] + sections = [] # type: List[Tuple[str, str]] nodeid = "test_node_id" log.pytest_sessionstart() - log.add_global_property("foo", 1) - log.add_global_property("bar", 2) + log.add_global_property("foo", "1") + log.add_global_property("bar", "2") log.pytest_sessionfinish() dom = minidom.parse(str(path)) @@ -1397,19 +1406,19 @@ class Report(BaseReport): assert actual == expected -def test_url_property(testdir): +def test_url_property(testdir) -> None: test_url = "http://www.github.com/pytest-dev" path = testdir.tmpdir.join("test_url_property.xml") log = LogXML(str(path), None) class Report(BaseReport): longrepr = "FooBarBaz" - sections = [] + sections = [] # type: List[Tuple[str, str]] nodeid = "something" location = "tests/filename.py", 42, "TestClass.method" url = test_url - test_report = Report() + test_report = cast(TestReport, Report()) log.pytest_sessionstart() node_reporter = log._opentestcase(test_report) diff --git a/testing/test_mark.py b/testing/test_mark.py index c14f770daa4..cdd4df9ddaa 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -13,14 +13,14 @@ class TestMark: @pytest.mark.parametrize("attr", ["mark", "param"]) @pytest.mark.parametrize("modulename", ["py.test", "pytest"]) - def test_pytest_exists_in_namespace_all(self, attr, modulename): + def test_pytest_exists_in_namespace_all(self, attr: str, modulename: str) -> None: module = sys.modules[modulename] - assert attr in module.__all__ + assert attr in module.__all__ # type: ignore - def test_pytest_mark_notcallable(self): + def test_pytest_mark_notcallable(self) -> None: mark = Mark() with pytest.raises(TypeError): - mark() + mark() # type: ignore[operator] # noqa: F821 def test_mark_with_param(self): def some_function(abc): @@ -30,10 +30,11 @@ class SomeClass: pass assert pytest.mark.foo(some_function) is some_function - assert pytest.mark.foo.with_args(some_function) is not some_function + marked_with_args = pytest.mark.foo.with_args(some_function) + assert marked_with_args is not some_function # type: ignore[comparison-overlap] # noqa: F821 assert pytest.mark.foo(SomeClass) is SomeClass - assert pytest.mark.foo.with_args(SomeClass) is not SomeClass + assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap] # noqa: F821 def test_pytest_mark_name_starts_with_underscore(self): mark = Mark() @@ -1044,9 +1045,9 @@ def test_custom_mark_parametrized(obj_type): result.assert_outcomes(passed=4) -def test_pytest_param_id_requires_string(): +def test_pytest_param_id_requires_string() -> None: with pytest.raises(TypeError) as excinfo: - pytest.param(id=True) + pytest.param(id=True) # type: ignore[arg-type] # noqa: F821 (msg,) = excinfo.value.args assert msg == "Expected id to be a string, got : True" diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 8c2fceb3fc2..1a3afbea9d8 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -2,6 +2,8 @@ import re import sys import textwrap +from typing import Dict +from typing import Generator import pytest from _pytest.compat import TYPE_CHECKING @@ -12,7 +14,7 @@ @pytest.fixture -def mp(): +def mp() -> Generator[MonkeyPatch, None, None]: cwd = os.getcwd() sys_path = list(sys.path) yield MonkeyPatch() @@ -20,14 +22,14 @@ def mp(): os.chdir(cwd) -def test_setattr(): +def test_setattr() -> None: class A: x = 1 monkeypatch = MonkeyPatch() pytest.raises(AttributeError, monkeypatch.setattr, A, "notexists", 2) monkeypatch.setattr(A, "y", 2, raising=False) - assert A.y == 2 + assert A.y == 2 # type: ignore monkeypatch.undo() assert not hasattr(A, "y") @@ -49,17 +51,17 @@ def test_string_expression(self, monkeypatch): monkeypatch.setattr("os.path.abspath", lambda x: "hello2") assert os.path.abspath("123") == "hello2" - def test_string_expression_class(self, monkeypatch): + def test_string_expression_class(self, monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr("_pytest.config.Config", 42) import _pytest - assert _pytest.config.Config == 42 + assert _pytest.config.Config == 42 # type: ignore - def test_unicode_string(self, monkeypatch): + def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr("_pytest.config.Config", 42) import _pytest - assert _pytest.config.Config == 42 + assert _pytest.config.Config == 42 # type: ignore monkeypatch.delattr("_pytest.config.Config") def test_wrong_target(self, monkeypatch): @@ -73,10 +75,10 @@ def test_unknown_attr(self, monkeypatch): AttributeError, lambda: monkeypatch.setattr("os.path.qweqwe", None) ) - def test_unknown_attr_non_raising(self, monkeypatch): + def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None: # https://github.com/pytest-dev/pytest/issues/746 monkeypatch.setattr("os.path.qweqwe", 42, raising=False) - assert os.path.qweqwe == 42 + assert os.path.qweqwe == 42 # type: ignore def test_delattr(self, monkeypatch): monkeypatch.delattr("os.path.abspath") @@ -123,8 +125,8 @@ def test_setitem(): assert d["x"] == 5 -def test_setitem_deleted_meanwhile(): - d = {} +def test_setitem_deleted_meanwhile() -> None: + d = {} # type: Dict[str, object] monkeypatch = MonkeyPatch() monkeypatch.setitem(d, "x", 2) del d["x"] @@ -148,8 +150,8 @@ def test_setenv_deleted_meanwhile(before): assert key not in os.environ -def test_delitem(): - d = {"x": 1} +def test_delitem() -> None: + d = {"x": 1} # type: Dict[str, object] monkeypatch = MonkeyPatch() monkeypatch.delitem(d, "x") assert "x" not in d @@ -241,7 +243,7 @@ def test_method(monkeypatch): assert tuple(res) == (1, 0, 0), res -def test_syspath_prepend(mp): +def test_syspath_prepend(mp: MonkeyPatch): old = list(sys.path) mp.syspath_prepend("world") mp.syspath_prepend("hello") @@ -253,7 +255,7 @@ def test_syspath_prepend(mp): assert sys.path == old -def test_syspath_prepend_double_undo(mp): +def test_syspath_prepend_double_undo(mp: MonkeyPatch): old_syspath = sys.path[:] try: mp.syspath_prepend("hello world") @@ -265,24 +267,24 @@ def test_syspath_prepend_double_undo(mp): sys.path[:] = old_syspath -def test_chdir_with_path_local(mp, tmpdir): +def test_chdir_with_path_local(mp: MonkeyPatch, tmpdir): mp.chdir(tmpdir) assert os.getcwd() == tmpdir.strpath -def test_chdir_with_str(mp, tmpdir): +def test_chdir_with_str(mp: MonkeyPatch, tmpdir): mp.chdir(tmpdir.strpath) assert os.getcwd() == tmpdir.strpath -def test_chdir_undo(mp, tmpdir): +def test_chdir_undo(mp: MonkeyPatch, tmpdir): cwd = os.getcwd() mp.chdir(tmpdir) mp.undo() assert os.getcwd() == cwd -def test_chdir_double_undo(mp, tmpdir): +def test_chdir_double_undo(mp: MonkeyPatch, tmpdir): mp.chdir(tmpdir.strpath) mp.undo() tmpdir.chdir() diff --git a/testing/test_nodes.py b/testing/test_nodes.py index 5bd31b34261..e5d8ffd713b 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -2,6 +2,7 @@ import pytest from _pytest import nodes +from _pytest.pytester import Testdir @pytest.mark.parametrize( @@ -17,19 +18,19 @@ ("foo/bar", "foo/bar::TestBop", True), ), ) -def test_ischildnode(baseid, nodeid, expected): +def test_ischildnode(baseid: str, nodeid: str, expected: bool) -> None: result = nodes.ischildnode(baseid, nodeid) assert result is expected -def test_node_from_parent_disallowed_arguments(): +def test_node_from_parent_disallowed_arguments() -> None: with pytest.raises(TypeError, match="session is"): - nodes.Node.from_parent(None, session=None) + nodes.Node.from_parent(None, session=None) # type: ignore[arg-type] # noqa: F821 with pytest.raises(TypeError, match="config is"): - nodes.Node.from_parent(None, config=None) + nodes.Node.from_parent(None, config=None) # type: ignore[arg-type] # noqa: F821 -def test_std_warn_not_pytestwarning(testdir): +def test_std_warn_not_pytestwarning(testdir: Testdir) -> None: items = testdir.getitems( """ def test(): @@ -40,24 +41,24 @@ def test(): items[0].warn(UserWarning("some warning")) -def test__check_initialpaths_for_relpath(): +def test__check_initialpaths_for_relpath() -> None: """Ensure that it handles dirs, and does not always use dirname.""" cwd = py.path.local() - class FakeSession: + class FakeSession1: _initialpaths = [cwd] - assert nodes._check_initialpaths_for_relpath(FakeSession, cwd) == "" + assert nodes._check_initialpaths_for_relpath(FakeSession1, cwd) == "" sub = cwd.join("file") - class FakeSession: + class FakeSession2: _initialpaths = [cwd] - assert nodes._check_initialpaths_for_relpath(FakeSession, sub) == "file" + assert nodes._check_initialpaths_for_relpath(FakeSession2, sub) == "file" outside = py.path.local("/outside") - assert nodes._check_initialpaths_for_relpath(FakeSession, outside) is None + assert nodes._check_initialpaths_for_relpath(FakeSession2, outside) is None def test_failure_with_changed_cwd(testdir): diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 86a42f9e8a1..0701641f805 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -1,10 +1,13 @@ +from typing import List +from typing import Union + import pytest class TestPasteCapture: @pytest.fixture - def pastebinlist(self, monkeypatch, request): - pastebinlist = [] + def pastebinlist(self, monkeypatch, request) -> List[Union[str, bytes]]: + pastebinlist = [] # type: List[Union[str, bytes]] plugin = request.config.pluginmanager.getplugin("pastebin") monkeypatch.setattr(plugin, "create_new_paste", pastebinlist.append) return pastebinlist diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 336f468a8c4..713687578f4 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -1,6 +1,7 @@ import os import sys import types +from typing import List import pytest from _pytest.config import ExitCode @@ -10,7 +11,7 @@ @pytest.fixture -def pytestpm(): +def pytestpm() -> PytestPluginManager: return PytestPluginManager() @@ -86,7 +87,7 @@ def pytest_configure(self): config.pluginmanager.register(A()) assert len(values) == 2 - def test_hook_tracing(self, _config_for_test): + def test_hook_tracing(self, _config_for_test) -> None: pytestpm = _config_for_test.pluginmanager # fully initialized with plugins saveindent = [] @@ -99,7 +100,7 @@ def pytest_plugin_registered(self): saveindent.append(pytestpm.trace.root.indent) raise ValueError() - values = [] + values = [] # type: List[str] pytestpm.trace.root.setwriter(values.append) undo = pytestpm.enable_tracing() try: @@ -215,20 +216,20 @@ def test_canonical_import(self, monkeypatch): assert pm.get_plugin("pytest_xyz") == mod assert pm.is_registered(mod) - def test_consider_module(self, testdir, pytestpm): + def test_consider_module(self, testdir, pytestpm: PytestPluginManager) -> None: testdir.syspathinsert() testdir.makepyfile(pytest_p1="#") testdir.makepyfile(pytest_p2="#") mod = types.ModuleType("temp") - mod.pytest_plugins = ["pytest_p1", "pytest_p2"] + mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"] pytestpm.consider_module(mod) assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1" assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2" - def test_consider_module_import_module(self, testdir, _config_for_test): + def test_consider_module_import_module(self, testdir, _config_for_test) -> None: pytestpm = _config_for_test.pluginmanager mod = types.ModuleType("x") - mod.pytest_plugins = "pytest_a" + mod.__dict__["pytest_plugins"] = "pytest_a" aplugin = testdir.makepyfile(pytest_a="#") reprec = testdir.make_hook_recorder(pytestpm) testdir.syspathinsert(aplugin.dirpath()) diff --git a/testing/test_reports.py b/testing/test_reports.py index 81778e27d4b..08ac014a40b 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -32,7 +32,7 @@ def test_b(): pass assert test_b_call.outcome == "passed" assert test_b_call._to_json()["longrepr"] is None - def test_xdist_report_longrepr_reprcrash_130(self, testdir): + def test_xdist_report_longrepr_reprcrash_130(self, testdir) -> None: """Regarding issue pytest-xdist#130 This test came originally from test_remote.py in xdist (ca03269). @@ -50,6 +50,7 @@ def test_fail(): rep.longrepr.sections.append(added_section) d = rep._to_json() a = TestReport._from_json(d) + assert a.longrepr is not None # Check assembled == rep assert a.__dict__.keys() == rep.__dict__.keys() for key in rep.__dict__.keys(): @@ -67,7 +68,7 @@ def test_fail(): # Missing section attribute PR171 assert added_section in a.longrepr.sections - def test_reprentries_serialization_170(self, testdir): + def test_reprentries_serialization_170(self, testdir) -> None: """Regarding issue pytest-xdist#170 This test came originally from test_remote.py in xdist (ca03269). @@ -87,6 +88,7 @@ def test_repr_entry(): rep = reports[1] d = rep._to_json() a = TestReport._from_json(d) + assert a.longrepr is not None rep_entries = rep.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries @@ -102,7 +104,7 @@ def test_repr_entry(): assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines assert rep_entries[i].style == a_entries[i].style - def test_reprentries_serialization_196(self, testdir): + def test_reprentries_serialization_196(self, testdir) -> None: """Regarding issue pytest-xdist#196 This test came originally from test_remote.py in xdist (ca03269). @@ -122,6 +124,7 @@ def test_repr_entry_native(): rep = reports[1] d = rep._to_json() a = TestReport._from_json(d) + assert a.longrepr is not None rep_entries = rep.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries @@ -157,6 +160,7 @@ def test_xfail_imperative(): assert newrep.failed == rep.failed assert newrep.skipped == rep.skipped if newrep.skipped and not hasattr(newrep, "wasxfail"): + assert newrep.longrepr is not None assert len(newrep.longrepr) == 3 assert newrep.outcome == rep.outcome assert newrep.when == rep.when @@ -316,7 +320,7 @@ def check_longrepr(longrepr): # elsewhere and we do check the contents of the longrepr object after loading it. loaded_report.longrepr.toterminal(tw_mock) - def test_chained_exceptions_no_reprcrash(self, testdir, tw_mock): + def test_chained_exceptions_no_reprcrash(self, testdir, tw_mock) -> None: """Regression test for tracebacks without a reprcrash (#5971) This happens notably on exceptions raised by multiprocess.pool: the exception transfer @@ -367,7 +371,7 @@ def test_a(): reports = reprec.getreports("pytest_runtest_logreport") - def check_longrepr(longrepr): + def check_longrepr(longrepr) -> None: assert isinstance(longrepr, ExceptionChainRepr) assert len(longrepr.chain) == 2 entry1, entry2 = longrepr.chain @@ -378,6 +382,7 @@ def check_longrepr(longrepr): assert "ValueError: value error" in str(tb2) assert fileloc1 is None + assert fileloc2 is not None assert fileloc2.message == "ValueError: value error" # 3 reports: setup/call/teardown: get the call report @@ -394,6 +399,7 @@ def check_longrepr(longrepr): check_longrepr(loaded_report.longrepr) # for same reasons as previous test, ensure we don't blow up here + assert loaded_report.longrepr is not None loaded_report.longrepr.toterminal(tw_mock) def test_report_prevent_ConftestImportFailure_hiding_exception(self, testdir): diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index 0ff508d2c4d..1b5d9737177 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -2,6 +2,8 @@ test correct setup/teardowns at module, class, and instance level """ +from typing import List + import pytest @@ -242,12 +244,12 @@ def test_function2(hello): @pytest.mark.parametrize("arg", ["", "arg"]) def test_setup_teardown_function_level_with_optional_argument( - testdir, monkeypatch, arg -): + testdir, monkeypatch, arg: str, +) -> None: """parameter to setup/teardown xunit-style functions parameter is now optional (#1728).""" import sys - trace_setups_teardowns = [] + trace_setups_teardowns = [] # type: List[str] monkeypatch.setattr( sys, "trace_setups_teardowns", trace_setups_teardowns, raising=False ) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index f48e7836459..a6f1a9c09b5 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -98,7 +98,7 @@ def test_func(): expl = ev.getexplanation() assert expl == "condition: not hasattr(os, 'murks')" - def test_marked_skip_with_not_string(self, testdir): + def test_marked_skip_with_not_string(self, testdir) -> None: item = testdir.getitem( """ import pytest @@ -109,6 +109,7 @@ def test_func(): ) ev = MarkEvaluator(item, "skipif") exc = pytest.raises(pytest.fail.Exception, ev.istrue) + assert exc.value.msg is not None assert ( """Failed: you need to specify reason=STRING when using booleans as conditions.""" in exc.value.msg @@ -869,7 +870,7 @@ def test_foo(): result.stdout.fnmatch_lines(["ERROR*test_foo*"]) -def test_errors_in_xfail_skip_expressions(testdir): +def test_errors_in_xfail_skip_expressions(testdir) -> None: testdir.makepyfile( """ import pytest @@ -886,7 +887,8 @@ def test_func(): ) result = testdir.runpytest() markline = " ^" - if hasattr(sys, "pypy_version_info") and sys.pypy_version_info < (6,): + 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:] diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 17fd29238f7..7d7c82ad68e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -6,6 +6,7 @@ import sys import textwrap from io import StringIO +from typing import cast from typing import Dict from typing import List from typing import Tuple @@ -17,9 +18,11 @@ import _pytest.terminal import pytest from _pytest._io.wcwidth import wcswidth +from _pytest.config import Config from _pytest.config import ExitCode from _pytest.pytester import Testdir from _pytest.reports import BaseReport +from _pytest.reports import CollectReport from _pytest.terminal import _folded_skips from _pytest.terminal import _get_line_with_reprcrash_message from _pytest.terminal import _plugin_nameversions @@ -1043,17 +1046,17 @@ def test_this(i): assert "collected 10 items" in result.stdout.str() -def test_getreportopt(): +def test_getreportopt() -> None: from _pytest.terminal import _REPORTCHARS_DEFAULT - class Config: + class FakeConfig: class Option: reportchars = _REPORTCHARS_DEFAULT disable_warnings = False option = Option() - config = Config() + config = cast(Config, FakeConfig()) assert _REPORTCHARS_DEFAULT == "fE" @@ -1994,7 +1997,7 @@ def test_xdist_normal(self, many_files, testdir, monkeypatch): output.stdout.re_match_lines([r"[\.E]{40} \s+ \[100%\]"]) -def test_skip_reasons_folding(): +def test_skip_reasons_folding() -> None: path = "xyz" lineno = 3 message = "justso" @@ -2003,28 +2006,28 @@ def test_skip_reasons_folding(): class X: pass - ev1 = X() + ev1 = cast(CollectReport, X()) ev1.when = "execute" ev1.skipped = True ev1.longrepr = longrepr - ev2 = X() + ev2 = cast(CollectReport, X()) ev2.when = "execute" ev2.longrepr = longrepr ev2.skipped = True # ev3 might be a collection report - ev3 = X() + ev3 = cast(CollectReport, X()) ev3.when = "collect" ev3.longrepr = longrepr ev3.skipped = True values = _folded_skips(py.path.local(), [ev1, ev2, ev3]) assert len(values) == 1 - num, fspath, lineno, reason = values[0] + num, fspath, lineno_, reason = values[0] assert num == 3 assert fspath == path - assert lineno == lineno + assert lineno_ == lineno assert reason == message @@ -2052,8 +2055,8 @@ class reprcrash: def check(msg, width, expected): __tracebackhide__ = True if msg: - rep.longrepr.reprcrash.message = msg - actual = _get_line_with_reprcrash_message(config, rep(), width) + rep.longrepr.reprcrash.message = msg # type: ignore + actual = _get_line_with_reprcrash_message(config, rep(), width) # type: ignore assert actual == expected if actual != "{} {}".format(mocked_verbose_word, mocked_pos): diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 3316751fb39..26a34c6565d 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -1,6 +1,8 @@ import os import stat import sys +from typing import Callable +from typing import List import attr @@ -263,10 +265,10 @@ def test_cleanup_lock_create(self, tmp_path): lockfile.unlink() - def test_lock_register_cleanup_removal(self, tmp_path): + def test_lock_register_cleanup_removal(self, tmp_path: Path) -> None: lock = create_cleanup_lock(tmp_path) - registry = [] + registry = [] # type: List[Callable[..., None]] register_cleanup_lock_removal(lock, register=registry.append) (cleanup_func,) = registry @@ -285,7 +287,7 @@ def test_lock_register_cleanup_removal(self, tmp_path): assert not lock.exists() - def _do_cleanup(self, tmp_path): + def _do_cleanup(self, tmp_path: Path) -> None: self.test_make(tmp_path) cleanup_numbered_dir( root=tmp_path, @@ -367,7 +369,7 @@ def test_rm_rf_with_read_only_directory(self, tmp_path): assert not adir.is_dir() - def test_on_rm_rf_error(self, tmp_path): + def test_on_rm_rf_error(self, tmp_path: Path) -> None: adir = tmp_path / "dir" adir.mkdir() @@ -377,32 +379,32 @@ def test_on_rm_rf_error(self, tmp_path): # unknown exception with pytest.warns(pytest.PytestWarning): - exc_info = (None, RuntimeError(), None) - on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path) + exc_info1 = (None, RuntimeError(), None) + on_rm_rf_error(os.unlink, str(fn), exc_info1, start_path=tmp_path) assert fn.is_file() # we ignore FileNotFoundError - exc_info = (None, FileNotFoundError(), None) - assert not on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path) + exc_info2 = (None, FileNotFoundError(), None) + assert not on_rm_rf_error(None, str(fn), exc_info2, start_path=tmp_path) # unknown function with pytest.warns( pytest.PytestWarning, match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\nNone: ", ): - exc_info = (None, PermissionError(), None) - on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path) + exc_info3 = (None, PermissionError(), None) + on_rm_rf_error(None, str(fn), exc_info3, start_path=tmp_path) assert fn.is_file() # ignored function with pytest.warns(None) as warninfo: - exc_info = (None, PermissionError(), None) - on_rm_rf_error(os.open, str(fn), exc_info, start_path=tmp_path) + exc_info4 = (None, PermissionError(), None) + on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path) assert fn.is_file() assert not [x.message for x in warninfo] - exc_info = (None, PermissionError(), None) - on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path) + exc_info5 = (None, PermissionError(), None) + on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path) assert not fn.is_file() diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 74a36c41bc0..6ddc6186be4 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1,4 +1,5 @@ import gc +from typing import List import pytest from _pytest.config import ExitCode @@ -1158,13 +1159,13 @@ def test(self): assert result.ret == 0 -def test_pdb_teardown_called(testdir, monkeypatch): +def test_pdb_teardown_called(testdir, monkeypatch) -> None: """Ensure tearDown() is always called when --pdb is given in the command-line. We delay the normal tearDown() calls when --pdb is given, so this ensures we are calling tearDown() eventually to avoid memory leaks when using --pdb. """ - teardowns = [] + teardowns = [] # type: List[str] monkeypatch.setattr( pytest, "test_pdb_teardown_called_teardowns", teardowns, raising=False ) @@ -1194,11 +1195,11 @@ def test_2(self): @pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"]) -def test_pdb_teardown_skipped(testdir, monkeypatch, mark): +def test_pdb_teardown_skipped(testdir, monkeypatch, mark: str) -> None: """ With --pdb, setUp and tearDown should not be called for skipped tests. """ - tracked = [] + tracked = [] # type: List[str] monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False) testdir.makepyfile( diff --git a/testing/test_warnings.py b/testing/test_warnings.py index ea7ab397dfe..e21ccf42ae8 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -1,5 +1,8 @@ import os import warnings +from typing import List +from typing import Optional +from typing import Tuple import pytest from _pytest.fixtures import FixtureRequest @@ -661,7 +664,9 @@ class TestStackLevel: @pytest.fixture def capwarn(self, testdir): class CapturedWarnings: - captured = [] + captured = ( + [] + ) # type: List[Tuple[warnings.WarningMessage, Optional[Tuple[str, int, str]]]] @classmethod def pytest_warning_recorded(cls, warning_message, when, nodeid, location): From 2b05faff0a0172dbc74b81f47528e56ad608839e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:17 +0300 Subject: [PATCH 325/823] Improve types around repr_failure() --- src/_pytest/_code/code.py | 2 +- src/_pytest/doctest.py | 5 ++++- src/_pytest/nodes.py | 24 +++++++++++++++--------- src/_pytest/python.py | 6 +++++- src/_pytest/runner.py | 6 ++++-- src/_pytest/skipping.py | 3 ++- 6 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 09b2c1af525..a40b2347076 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -47,7 +47,7 @@ from typing_extensions import Literal from weakref import ReferenceType - _TracebackStyle = Literal["long", "short", "line", "no", "native", "value"] + _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] class Code: diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index ab8085982af..7aaacb481c2 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -300,7 +300,10 @@ def _disable_output_capturing_for_darwin(self) -> None: sys.stdout.write(out) sys.stderr.write(err) - def repr_failure(self, excinfo): + # TODO: Type ignored -- breaks Liskov Substitution. + def repr_failure( # type: ignore[override] # noqa: F821 + self, excinfo: ExceptionInfo[BaseException], + ) -> Union[str, TerminalRepr]: import doctest failures = ( diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 15f91343fae..3757e0b2717 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -17,9 +17,8 @@ import _pytest._code from _pytest._code import getfslineno -from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo -from _pytest._code.code import ReprExceptionInfo +from _pytest._code.code import TerminalRepr from _pytest.compat import cached_property from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING @@ -29,7 +28,6 @@ from _pytest.deprecated import NODE_USE_FROM_PARENT from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError -from _pytest.fixtures import FixtureLookupErrorRepr from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords @@ -43,6 +41,7 @@ # Imported here due to circular import. from _pytest.main import Session from _pytest.warning_types import PytestWarning + from _pytest._code.code import _TracebackStyle SEP = "/" @@ -355,8 +354,10 @@ def _prunetraceback(self, excinfo): pass def _repr_failure_py( - self, excinfo: ExceptionInfo[BaseException], style=None, - ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: + self, + excinfo: ExceptionInfo[BaseException], + style: "Optional[_TracebackStyle]" = None, + ) -> TerminalRepr: if isinstance(excinfo.value, ConftestImportFailure): excinfo = ExceptionInfo(excinfo.value.excinfo) if isinstance(excinfo.value, fail.Exception): @@ -406,8 +407,10 @@ def _repr_failure_py( ) def repr_failure( - self, excinfo, style=None - ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: + self, + excinfo: ExceptionInfo[BaseException], + style: "Optional[_TracebackStyle]" = None, + ) -> Union[str, TerminalRepr]: """ Return a representation of a collection or test failure. @@ -453,13 +456,16 @@ def collect(self) -> Iterable[Union["Item", "Collector"]]: """ raise NotImplementedError("abstract") - def repr_failure(self, excinfo): + # TODO: This omits the style= parameter which breaks Liskov Substitution. + def repr_failure( # type: ignore[override] # noqa: F821 + self, excinfo: ExceptionInfo[BaseException] + ) -> Union[str, TerminalRepr]: """ Return a representation of a collection failure. :param excinfo: Exception information for the failure. """ - if excinfo.errisinstance(self.CollectError) and not self.config.getoption( + if isinstance(excinfo.value, self.CollectError) and not self.config.getoption( "fulltrace", False ): exc = excinfo.value diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 41dd8b292cc..4b716c616b6 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -60,6 +60,7 @@ from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.pathlib import parts +from _pytest.reports import TerminalRepr from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning @@ -1591,7 +1592,10 @@ def _prunetraceback(self, excinfo: ExceptionInfo) -> None: for entry in excinfo.traceback[1:-1]: entry.set_repr_style("short") - def repr_failure(self, excinfo, outerr=None): + # TODO: Type ignored -- breaks Liskov Substitution. + def repr_failure( # type: ignore[override] # noqa: F821 + self, excinfo: ExceptionInfo[BaseException], outerr: None = None + ) -> Union[str, TerminalRepr]: assert outerr is None, "XXX outerr usage is deprecated" style = self.config.getoption("tbstyle", "auto") if style == "auto": diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index f89b673991f..3ca8d7ea46d 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -2,6 +2,7 @@ import bdb import os import sys +from typing import Any from typing import Callable from typing import cast from typing import Dict @@ -256,7 +257,7 @@ class CallInfo(Generic[_T]): """ _result = attr.ib(type="Optional[_T]") - excinfo = attr.ib(type=Optional[ExceptionInfo]) + excinfo = attr.ib(type=Optional[ExceptionInfo[BaseException]]) start = attr.ib(type=float) stop = attr.ib(type=float) duration = attr.ib(type=float) @@ -313,7 +314,8 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: def pytest_make_collect_report(collector: Collector) -> CollectReport: call = CallInfo.from_call(lambda: list(collector.collect()), "collect") - longrepr = None + # TODO: Better typing for longrepr. + longrepr = None # type: Optional[Any] if not call.excinfo: outcome = "passed" # type: Literal["passed", "skipped", "failed"] else: diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 54621f111cc..bbd4593fd40 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -148,7 +148,8 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): elif item.config.option.runxfail: pass # don't interfere - elif call.excinfo and call.excinfo.errisinstance(xfail.Exception): + elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception): + assert call.excinfo.value.msg is not None rep.wasxfail = "reason: " + call.excinfo.value.msg rep.outcome = "skipped" elif evalxfail and not rep.skipped and evalxfail.wasvalid() and evalxfail.istrue(): From 19ad5889353c7f5f2b65cc2acd346b7a9e95dfcd Mon Sep 17 00:00:00 2001 From: Xinbin Huang Date: Fri, 5 Jun 2020 04:10:16 -0700 Subject: [PATCH 326/823] Add reference to builtin markers to doc (#7321) Co-authored-by: Bruno Oliveira --- doc/en/mark.rst | 11 ++++++++--- doc/en/start_doc_server.sh | 5 +++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 doc/en/start_doc_server.sh diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 3899dab88b1..6fb665fdfd2 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -4,14 +4,19 @@ Marking test functions with attributes ====================================== By using the ``pytest.mark`` helper you can easily set -metadata on your test functions. There are -some builtin markers, for example: +metadata on your test functions. You can find the full list of builtin markers +in the :ref:`API Reference`. Or you can list all the markers, including +builtin and custom, using the CLI - :code:`pytest --markers`. +Here are some of the builtin markers: + +* :ref:`usefixtures ` - use fixtures on a test function or class +* :ref:`filterwarnings ` - filter certain warnings of a test function * :ref:`skip ` - always skip a test function * :ref:`skipif ` - skip a test function if a certain condition is met * :ref:`xfail ` - produce an "expected failure" outcome if a certain condition is met -* :ref:`parametrize ` to perform multiple calls +* :ref:`parametrize ` - perform multiple calls to the same test function. It's easy to create custom markers or to apply markers diff --git a/doc/en/start_doc_server.sh b/doc/en/start_doc_server.sh new file mode 100644 index 00000000000..f68677409be --- /dev/null +++ b/doc/en/start_doc_server.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "${MY_DIR}"/_build/html || exit +python -m http.server 8000 From 1deaa743452acb147c0cf1f3629fc52599c28a1d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 5 Jun 2020 15:52:08 +0300 Subject: [PATCH 327/823] mark/expression: prevent creation of illegal Python identifiers This is rejected by Python DEBUG builds, as well as regular builds in future versions. --- src/_pytest/mark/expression.py | 10 ++++++++-- testing/test_mark_expression.py | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 04c73411af5..73b7bf16992 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -127,6 +127,12 @@ def reject(self, expected: Sequence[TokenType]) -> "NoReturn": ) +# True, False and None are legal match expression identifiers, +# but illegal as Python identifiers. To fix this, this prefix +# is added to identifiers in the conversion to Python AST. +IDENT_PREFIX = "$" + + def expression(s: Scanner) -> ast.Expression: if s.accept(TokenType.EOF): ret = ast.NameConstant(False) # type: ast.expr @@ -161,7 +167,7 @@ def not_expr(s: Scanner) -> ast.expr: return ret ident = s.accept(TokenType.IDENT) if ident: - return ast.Name(ident.value, ast.Load()) + return ast.Name(IDENT_PREFIX + ident.value, ast.Load()) s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) @@ -172,7 +178,7 @@ def __init__(self, matcher: Callable[[str], bool]) -> None: self.matcher = matcher def __getitem__(self, key: str) -> bool: - return self.matcher(key) + return self.matcher(key[len(IDENT_PREFIX) :]) def __iter__(self) -> Iterator[str]: raise NotImplementedError() diff --git a/testing/test_mark_expression.py b/testing/test_mark_expression.py index 335888618ad..faca02d9330 100644 --- a/testing/test_mark_expression.py +++ b/testing/test_mark_expression.py @@ -130,6 +130,7 @@ def test_syntax_errors(expr: str, column: int, message: str) -> None: "123.232", "True", "False", + "None", "if", "else", "while", From 2a3c21645e5c303a71694c0ff68d0a56c2d734d5 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 6 Jun 2020 02:38:18 -0400 Subject: [PATCH 328/823] Commit solution thus far, needs to be polished up pre PR --- doc/en/reference.rst | 10 +++++ src/_pytest/config/__init__.py | 33 +++++++++++++--- testing/test_config.py | 71 ++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 7348636a2dd..d84d9d4054a 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1604,3 +1604,13 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] xfail_strict = True + + +.. confval:: required_plugins + + A space seperated list of plugins that must be present for pytest to run + + .. code-block:: ini + + [pytest] + require_plugins = pluginA pluginB pluginC diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 27083900dfa..83878a486b0 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -952,6 +952,12 @@ def _initini(self, args: Sequence[str]) -> None: self._parser.extra_info["inifile"] = self.inifile self._parser.addini("addopts", "extra command line options", "args") self._parser.addini("minversion", "minimally required pytest version") + self._parser.addini( + "require_plugins", + "plugins that must be present for pytest to run", + type="args", + default=[], + ) self._override_ini = ns.override_ini or () def _consider_importhook(self, args: Sequence[str]) -> None: @@ -1035,7 +1041,8 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: self.known_args_namespace = ns = self._parser.parse_known_args( args, namespace=copy.copy(self.option) ) - self._validatekeys() + self._validate_keys() + self._validate_plugins() if self.known_args_namespace.confcutdir is None and self.inifile: confcutdir = py.path.local(self.inifile).dirname self.known_args_namespace.confcutdir = confcutdir @@ -1078,12 +1085,26 @@ def _checkversion(self): ) ) - def _validatekeys(self): + def _validate_keys(self) -> None: for key in sorted(self._get_unknown_ini_keys()): - message = "Unknown config ini key: {}\n".format(key) - if self.known_args_namespace.strict_config: - fail(message, pytrace=False) - sys.stderr.write("WARNING: {}".format(message)) + self._emit_warning_or_fail("Unknown config ini key: {}\n".format(key)) + + def _validate_plugins(self) -> None: + # so iterate over all required plugins and see if pluginmanager hasplugin + # NOTE: This also account for -p no: ( e.g: -p no:celery ) + # raise ValueError(self._parser._inidict['requiredplugins']) + # raise ValueError(self.getini("requiredplugins")) + # raise ValueError(self.pluginmanager.hasplugin('debugging')) + for plugin in self.getini("require_plugins"): + if not self.pluginmanager.hasplugin(plugin): + self._emit_warning_or_fail( + "Missing required plugin: {}\n".format(plugin) + ) + + def _emit_warning_or_fail(self, message: str) -> None: + if self.known_args_namespace.strict_config: + fail(message, pytrace=False) + sys.stderr.write("WARNING: {}".format(message)) def _get_unknown_ini_keys(self) -> List[str]: parser_inicfg = self._parser._inidict diff --git a/testing/test_config.py b/testing/test_config.py index 867012e932c..f88a9a0ce92 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -212,6 +212,77 @@ def test_invalid_ini_keys( with pytest.raises(pytest.fail.Exception, match=exception_text): testdir.runpytest("--strict-config") + @pytest.mark.parametrize( + "ini_file_text, stderr_output, exception_text", + [ + ( + """ + [pytest] + require_plugins = fakePlugin1 fakePlugin2 + """, + [ + "WARNING: Missing required plugin: fakePlugin1", + "WARNING: Missing required plugin: fakePlugin2", + ], + "Missing required plugin: fakePlugin1", + ), + ( + """ + [pytest] + require_plugins = a monkeypatch z + """, + [ + "WARNING: Missing required plugin: a", + "WARNING: Missing required plugin: z", + ], + "Missing required plugin: a", + ), + ( + """ + [pytest] + require_plugins = a monkeypatch z + addopts = -p no:monkeypatch + """, + [ + "WARNING: Missing required plugin: a", + "WARNING: Missing required plugin: monkeypatch", + "WARNING: Missing required plugin: z", + ], + "Missing required plugin: a", + ), + ( + """ + [some_other_header] + require_plugins = wont be triggered + [pytest] + minversion = 5.0.0 + """, + [], + "", + ), + ( + """ + [pytest] + minversion = 5.0.0 + """, + [], + "", + ), + ], + ) + def test_missing_required_plugins( + self, testdir, ini_file_text, stderr_output, exception_text + ): + testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) + testdir.parseconfig() + + result = testdir.runpytest() + result.stderr.fnmatch_lines(stderr_output) + + if stderr_output: + with pytest.raises(pytest.fail.Exception, match=exception_text): + testdir.runpytest("--strict-config") + class TestConfigCmdlineParsing: def test_parsing_again_fails(self, testdir): From f760b105efa12ebc14adccda3c840ad3a61936ef Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 6 Jun 2020 11:05:32 -0400 Subject: [PATCH 329/823] Touchup pre-PR --- changelog/7305.feature.rst | 3 +++ doc/en/reference.rst | 22 ++++++++++++---------- src/_pytest/config/__init__.py | 5 ----- 3 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 changelog/7305.feature.rst diff --git a/changelog/7305.feature.rst b/changelog/7305.feature.rst new file mode 100644 index 00000000000..cf5a48c6eba --- /dev/null +++ b/changelog/7305.feature.rst @@ -0,0 +1,3 @@ +A new INI key `require_plugins` has been added that allows the user to specify a list of plugins required for pytest to run. + +The `--strict-config` flag can be used to treat these warnings as errors. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index d84d9d4054a..1f1f2c423c2 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1561,6 +1561,18 @@ passed multiple times. The expected format is ``name=value``. For example:: See :ref:`change naming conventions` for more detailed examples. +.. confval:: require_plugins + + A space separated list of plugins that must be present for pytest to run. + If any one of the plugins is not found, emit a warning. + If pytest is run with ``--strict-config`` exceptions are raised in place of warnings. + + .. code-block:: ini + + [pytest] + require_plugins = pluginA pluginB pluginC + + .. confval:: testpaths @@ -1604,13 +1616,3 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] xfail_strict = True - - -.. confval:: required_plugins - - A space seperated list of plugins that must be present for pytest to run - - .. code-block:: ini - - [pytest] - require_plugins = pluginA pluginB pluginC diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 83878a486b0..7d077d2974f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1090,11 +1090,6 @@ def _validate_keys(self) -> None: self._emit_warning_or_fail("Unknown config ini key: {}\n".format(key)) def _validate_plugins(self) -> None: - # so iterate over all required plugins and see if pluginmanager hasplugin - # NOTE: This also account for -p no: ( e.g: -p no:celery ) - # raise ValueError(self._parser._inidict['requiredplugins']) - # raise ValueError(self.getini("requiredplugins")) - # raise ValueError(self.pluginmanager.hasplugin('debugging')) for plugin in self.getini("require_plugins"): if not self.pluginmanager.hasplugin(plugin): self._emit_warning_or_fail( From 3f6b3e7faa49c891e0b3036f07873296a73c8618 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 6 Jun 2020 11:33:28 -0400 Subject: [PATCH 330/823] update help for --strict-config --- src/_pytest/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 1c1cda18bdf..fd39b6ad70f 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -78,7 +78,7 @@ def pytest_addoption(parser: Parser) -> None: group._addoption( "--strict-config", action="store_true", - help="invalid ini keys for the `pytest` section of the configuration file raise errors.", + help="any warnings encountered while parsing the `pytest` section of the configuration file raise errors.", ) group._addoption( "--strict-markers", From ceac6736d772c68ebfbde21dcabcd179068f8498 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Jun 2020 19:17:40 -0300 Subject: [PATCH 331/823] Fix mention using --rootdir mention inside pytest.ini (not supported) (#6825) Co-authored-by: Ran Benita --- doc/en/customize.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/en/customize.rst b/doc/en/customize.rst index 9554ab7b518..35c851ebb62 100644 --- a/doc/en/customize.rst +++ b/doc/en/customize.rst @@ -39,8 +39,9 @@ Here's a summary what ``pytest`` uses ``rootdir`` for: influence how modules are imported. See :ref:`pythonpath` for more details. The ``--rootdir=path`` command-line option can be used to force a specific directory. -The directory passed may contain environment variables when it is used in conjunction -with ``addopts`` in a ``pytest.ini`` file. +Note that contrary to other command-line options, ``--rootdir`` cannot be used with +:confval:`addopts` inside ``pytest.ini`` because the ``rootdir`` is used to *find* ``pytest.ini`` +already. Finding the ``rootdir`` ~~~~~~~~~~~~~~~~~~~~~~~ From 42deba59e7d6cfe596414d0beff6fafaa14b02a3 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 6 Jun 2020 22:34:15 -0400 Subject: [PATCH 332/823] Update documentation as suggested --- changelog/7305.feature.rst | 2 +- doc/en/reference.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/7305.feature.rst b/changelog/7305.feature.rst index cf5a48c6eba..8e8ae85ae8e 100644 --- a/changelog/7305.feature.rst +++ b/changelog/7305.feature.rst @@ -1,3 +1,3 @@ -A new INI key `require_plugins` has been added that allows the user to specify a list of plugins required for pytest to run. +New `require_plugins` configuration option allows the user to specify a list of plugins required for pytest to run. The `--strict-config` flag can be used to treat these warnings as errors. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 1f1f2c423c2..dc82fe23927 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1570,7 +1570,7 @@ passed multiple times. The expected format is ``name=value``. For example:: .. code-block:: ini [pytest] - require_plugins = pluginA pluginB pluginC + require_plugins = pytest-xdist pytest-mock .. confval:: testpaths From d2bb67bfdafcbadd39f9551a52635188f54954e0 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 7 Jun 2020 14:10:20 -0400 Subject: [PATCH 333/823] validate plugins before keys in config files --- src/_pytest/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 7d077d2974f..d55a5cdd7d5 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1041,8 +1041,8 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: self.known_args_namespace = ns = self._parser.parse_known_args( args, namespace=copy.copy(self.option) ) - self._validate_keys() self._validate_plugins() + self._validate_keys() if self.known_args_namespace.confcutdir is None and self.inifile: confcutdir = py.path.local(self.inifile).dirname self.known_args_namespace.confcutdir = confcutdir From 13add4df43eef412bf7369926345e62eca0624b1 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 7 Jun 2020 15:37:50 -0400 Subject: [PATCH 334/823] documentation fixes --- changelog/7305.feature.rst | 2 +- doc/en/reference.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/7305.feature.rst b/changelog/7305.feature.rst index 8e8ae85ae8e..b8c0ca693c7 100644 --- a/changelog/7305.feature.rst +++ b/changelog/7305.feature.rst @@ -1,3 +1,3 @@ -New `require_plugins` configuration option allows the user to specify a list of plugins required for pytest to run. +New `require_plugins` configuration option allows the user to specify a list of plugins required for pytest to run. Warnings are raised if these plugins are not found when running pytest. The `--strict-config` flag can be used to treat these warnings as errors. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index dc82fe23927..6b270796c8e 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1570,7 +1570,7 @@ passed multiple times. The expected format is ``name=value``. For example:: .. code-block:: ini [pytest] - require_plugins = pytest-xdist pytest-mock + require_plugins = html xdist .. confval:: testpaths From c17d50829f3173c85a9810520458a112971d551c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 8 Jun 2020 10:03:10 -0300 Subject: [PATCH 335/823] Add pyproject.toml support (#7247) --- .gitignore | 1 + changelog/1556.feature.rst | 17 +++ doc/en/customize.rst | 219 ++++++++++++++++++---------- doc/en/example/pythoncollection.rst | 7 +- doc/en/example/simple.rst | 44 ++++++ doc/en/reference.rst | 14 +- pyproject.toml | 43 ++++++ setup.cfg | 1 + src/_pytest/config/__init__.py | 51 ++++--- src/_pytest/config/findpaths.py | 140 +++++++++++------- src/_pytest/pytester.py | 7 + src/_pytest/terminal.py | 2 +- testing/test_config.py | 139 +++++++++++++++--- testing/test_findpaths.py | 110 ++++++++++++++ testing/test_terminal.py | 8 +- tox.ini | 42 ------ 16 files changed, 609 insertions(+), 236 deletions(-) create mode 100644 changelog/1556.feature.rst create mode 100644 testing/test_findpaths.py diff --git a/.gitignore b/.gitignore index 83b6dbe7351..faea9eac03f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ doc/*/_changelog_towncrier_draft.rst build/ dist/ *.egg-info +htmlcov/ issue/ env/ .env/ diff --git a/changelog/1556.feature.rst b/changelog/1556.feature.rst new file mode 100644 index 00000000000..402e772e674 --- /dev/null +++ b/changelog/1556.feature.rst @@ -0,0 +1,17 @@ +pytest now supports ``pyproject.toml`` files for configuration. + +The configuration options is similar to the one available in other formats, but must be defined +in a ``[tool.pytest.ini_options]`` table to be picked up by pytest: + +.. code-block:: toml + + # pyproject.toml + [tool.pytest.ini_options] + minversion = "6.0" + addopts = "-ra -q" + testpaths = [ + "tests", + "integration", + ] + +More information can be found `in the docs `__. diff --git a/doc/en/customize.rst b/doc/en/customize.rst index 35c851ebb62..e1f1b253bc9 100644 --- a/doc/en/customize.rst +++ b/doc/en/customize.rst @@ -14,15 +14,112 @@ configurations files by using the general help option: This will display command line and configuration file settings which were registered by installed plugins. +.. _`config file formats`: + +Configuration file formats +-------------------------- + +Many :ref:`pytest settings ` can be set in a *configuration file*, which +by convention resides on the root of your repository or in your +tests folder. + +A quick example of the configuration files supported by pytest: + +pytest.ini +~~~~~~~~~~ + +``pytest.ini`` files take precedence over other files, even when empty. + +.. code-block:: ini + + # pytest.ini + [pytest] + minversion = 6.0 + addopts = -ra -q + testpaths = + tests + integration + + +pyproject.toml +~~~~~~~~~~~~~~ + +.. versionadded:: 6.0 + +``pyproject.toml`` are considered for configuration when they contain a ``tool.pytest.ini_options`` table. + +.. code-block:: toml + + # pyproject.toml + [tool.pytest.ini_options] + minversion = "6.0" + addopts = "-ra -q" + testpaths = [ + "tests", + "integration", + ] + +.. note:: + + One might wonder why ``[tool.pytest.ini_options]`` instead of ``[tool.pytest]`` as is the + case with other tools. + + The reason is that the pytest team intends to fully utilize the rich TOML data format + for configuration in the future, reserving the ``[tool.pytest]`` table for that. + The ``ini_options`` table is being used, for now, as a bridge between the existing + ``.ini`` configuration system and the future configuration format. + +tox.ini +~~~~~~~ + +``tox.ini`` files are the configuration files of the `tox `__ project, +and can also be used to hold pytest configuration if they have a ``[pytest]`` section. + +.. code-block:: ini + + # tox.ini + [pytest] + minversion = 6.0 + addopts = -ra -q + testpaths = + tests + integration + + +setup.cfg +~~~~~~~~~ + +``setup.cfg`` files are general purpose configuration files, used originally by `distutils `__, and can also be used to hold pytest configuration +if they have a ``[tool:pytest]`` section. + +.. code-block:: ini + + # setup.cfg + [tool:pytest] + minversion = 6.0 + addopts = -ra -q + testpaths = + tests + integration + +.. warning:: + + Usage of ``setup.cfg`` is not recommended unless for very simple use cases. ``.cfg`` + files use a different parser than ``pytest.ini`` and ``tox.ini`` which might cause hard to track + down problems. + When possible, it is recommended to use the latter files, or ``pyproject.toml``, to hold your + pytest configuration. + + .. _rootdir: -.. _inifiles: +.. _configfiles: -Initialization: determining rootdir and inifile ------------------------------------------------ +Initialization: determining rootdir and configfile +-------------------------------------------------- pytest determines a ``rootdir`` for each test run which depends on the command line arguments (specified test files, paths) and on -the existence of *ini-files*. The determined ``rootdir`` and *ini-file* are +the existence of configuration files. The determined ``rootdir`` and ``configfile`` are printed as part of the pytest header during startup. Here's a summary what ``pytest`` uses ``rootdir`` for: @@ -48,48 +145,47 @@ Finding the ``rootdir`` Here is the algorithm which finds the rootdir from ``args``: -- determine the common ancestor directory for the specified ``args`` that are +- Determine the common ancestor directory for the specified ``args`` that are recognised as paths that exist in the file system. If no such paths are found, the common ancestor directory is set to the current working directory. -- look for ``pytest.ini``, ``tox.ini`` and ``setup.cfg`` files in the ancestor - directory and upwards. If one is matched, it becomes the ini-file and its - directory becomes the rootdir. +- Look for ``pytest.ini``, ``pyproject.toml``, ``tox.ini``, and ``setup.cfg`` files in the ancestor + directory and upwards. If one is matched, it becomes the ``configfile`` and its + directory becomes the ``rootdir``. -- if no ini-file was found, look for ``setup.py`` upwards from the common +- If no configuration file was found, look for ``setup.py`` upwards from the common ancestor directory to determine the ``rootdir``. -- if no ``setup.py`` was found, look for ``pytest.ini``, ``tox.ini`` and +- If no ``setup.py`` was found, look for ``pytest.ini``, ``pyproject.toml``, ``tox.ini``, and ``setup.cfg`` in each of the specified ``args`` and upwards. If one is - matched, it becomes the ini-file and its directory becomes the rootdir. + matched, it becomes the ``configfile`` and its directory becomes the ``rootdir``. -- if no ini-file was found, use the already determined common ancestor as root +- If no ``configfile`` was found, use the already determined common ancestor as root directory. This allows the use of pytest in structures that are not part of - a package and don't have any particular ini-file configuration. + a package and don't have any particular configuration file. If no ``args`` are given, pytest collects test below the current working -directory and also starts determining the rootdir from there. +directory and also starts determining the ``rootdir`` from there. -:warning: custom pytest plugin commandline arguments may include a path, as in - ``pytest --log-output ../../test.log args``. Then ``args`` is mandatory, - otherwise pytest uses the folder of test.log for rootdir determination - (see also `issue 1435 `_). - A dot ``.`` for referencing to the current working directory is also - possible. +Files will only be matched for configuration if: -Note that an existing ``pytest.ini`` file will always be considered a match, -whereas ``tox.ini`` and ``setup.cfg`` will only match if they contain a -``[pytest]`` or ``[tool:pytest]`` section, respectively. Options from multiple ini-files candidates are never -merged - the first one wins (``pytest.ini`` always wins, even if it does not -contain a ``[pytest]`` section). +* ``pytest.ini``: will always match and take precedence, even if empty. +* ``pyproject.toml``: contains a ``[tool.pytest.ini_options]`` table. +* ``tox.ini``: contains a ``[pytest]`` section. +* ``setup.cfg``: contains a ``[tool:pytest]`` section. -The ``config`` object will subsequently carry these attributes: +The files are considered in the order above. Options from multiple ``configfiles`` candidates +are never merged - the first match wins. + +The internal :class:`Config <_pytest.config.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture) +will subsequently carry these attributes: - ``config.rootdir``: the determined root directory, guaranteed to exist. -- ``config.inifile``: the determined ini-file, may be ``None``. +- ``config.inifile``: the determined ``configfile``, may be ``None`` (it is named ``inifile`` + for historical reasons). -The rootdir is used as a reference directory for constructing test +The ``rootdir`` is used as a reference directory for constructing test addresses ("nodeids") and can be used also by plugins for storing per-testrun information. @@ -100,73 +196,36 @@ Example: pytest path/to/testdir path/other/ will determine the common ancestor as ``path`` and then -check for ini-files as follows: +check for configuration files as follows: .. code-block:: text # first look for pytest.ini files path/pytest.ini - path/tox.ini # must also contain [pytest] section to match - path/setup.cfg # must also contain [tool:pytest] section to match + path/pyproject.toml # must contain a [tool.pytest.ini_options] table to match + path/tox.ini # must contain [pytest] section to match + path/setup.cfg # must contain [tool:pytest] section to match pytest.ini - ... # all the way down to the root + ... # all the way up to the root # now look for setup.py path/setup.py setup.py - ... # all the way down to the root - - -.. _`how to change command line options defaults`: -.. _`adding default options`: - - + ... # all the way up to the root -How to change command line options defaults ------------------------------------------------- -It can be tedious to type the same series of command line options -every time you use ``pytest``. For example, if you always want to see -detailed info on skipped and xfailed tests, as well as have terser "dot" -progress output, you can write it into a configuration file: +.. warning:: -.. code-block:: ini - - # content of pytest.ini or tox.ini - [pytest] - addopts = -ra -q - - # content of setup.cfg - [tool:pytest] - addopts = -ra -q - -Alternatively, you can set a ``PYTEST_ADDOPTS`` environment variable to add command -line options while the environment is in use: - -.. code-block:: bash - - export PYTEST_ADDOPTS="-v" - -Here's how the command-line is built in the presence of ``addopts`` or the environment variable: - -.. code-block:: text - - $PYTEST_ADDOPTS - -So if the user executes in the command-line: - -.. code-block:: bash - - pytest -m slow - -The actual command line executed is: - -.. code-block:: bash + Custom pytest plugin commandline arguments may include a path, as in + ``pytest --log-output ../../test.log args``. Then ``args`` is mandatory, + otherwise pytest uses the folder of test.log for rootdir determination + (see also `issue 1435 `_). + A dot ``.`` for referencing to the current working directory is also + possible. - pytest -ra -q -v -m slow -Note that as usual for other command-line applications, in case of conflicting options the last one wins, so the example -above will show verbose output because ``-v`` overwrites ``-q``. +.. _`how to change command line options defaults`: +.. _`adding default options`: Builtin configuration file options diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index d8261a94928..30d106adab6 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -115,15 +115,13 @@ Changing naming conventions You can configure different naming conventions by setting the :confval:`python_files`, :confval:`python_classes` and -:confval:`python_functions` configuration options. +:confval:`python_functions` in your :ref:`configuration file `. Here is an example: .. code-block:: ini # content of pytest.ini # Example 1: have pytest look for "check" instead of "test" - # can also be defined in tox.ini or setup.cfg file, although the section - # name in setup.cfg files should be "tool:pytest" [pytest] python_files = check_*.py python_classes = Check @@ -165,8 +163,7 @@ You can check for multiple glob patterns by adding a space between the patterns: .. code-block:: ini # Example 2: have pytest look for files with "test" and "example" - # content of pytest.ini, tox.ini, or setup.cfg file (replace "pytest" - # with "tool:pytest" for setup.cfg) + # content of pytest.ini [pytest] python_files = test_*.py example_*.py diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 3282bbda584..d1a1ecdfc9d 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -3,6 +3,50 @@ Basic patterns and examples ========================================================== +How to change command line options defaults +------------------------------------------- + +It can be tedious to type the same series of command line options +every time you use ``pytest``. For example, if you always want to see +detailed info on skipped and xfailed tests, as well as have terser "dot" +progress output, you can write it into a configuration file: + +.. code-block:: ini + + # content of pytest.ini + [pytest] + addopts = -ra -q + + +Alternatively, you can set a ``PYTEST_ADDOPTS`` environment variable to add command +line options while the environment is in use: + +.. code-block:: bash + + export PYTEST_ADDOPTS="-v" + +Here's how the command-line is built in the presence of ``addopts`` or the environment variable: + +.. code-block:: text + + $PYTEST_ADDOPTS + +So if the user executes in the command-line: + +.. code-block:: bash + + pytest -m slow + +The actual command line executed is: + +.. code-block:: bash + + pytest -ra -q -v -m slow + +Note that as usual for other command-line applications, in case of conflicting options the last one wins, so the example +above will show verbose output because ``-v`` overwrites ``-q``. + + .. _request example: Pass different values to a test function, depending on command line options diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 7348636a2dd..326b3e52add 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1019,17 +1019,17 @@ UsageError Configuration Options --------------------- -Here is a list of builtin configuration options that may be written in a ``pytest.ini``, ``tox.ini`` or ``setup.cfg`` -file, usually located at the root of your repository. All options must be under a ``[pytest]`` section -(``[tool:pytest]`` for ``setup.cfg`` files). +Here is a list of builtin configuration options that may be written in a ``pytest.ini``, ``pyproject.toml``, ``tox.ini`` or ``setup.cfg`` +file, usually located at the root of your repository. To see each file format in details, see +:ref:`config file formats`. .. warning:: - Usage of ``setup.cfg`` is not recommended unless for very simple use cases. ``.cfg`` + Usage of ``setup.cfg`` is not recommended except for very simple use cases. ``.cfg`` files use a different parser than ``pytest.ini`` and ``tox.ini`` which might cause hard to track down problems. - When possible, it is recommended to use the latter files to hold your pytest configuration. + When possible, it is recommended to use the latter files, or ``pyproject.toml``, to hold your pytest configuration. -Configuration file options may be overwritten in the command-line by using ``-o/--override-ini``, which can also be +Configuration options may be overwritten in the command-line by using ``-o/--override-ini``, which can also be passed multiple times. The expected format is ``name=value``. For example:: pytest -o console_output_style=classic -o cache_dir=/tmp/mycache @@ -1057,8 +1057,6 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: cache_dir - - Sets a directory where stores content of cache plugin. Default directory is ``.pytest_cache`` which is created in :ref:`rootdir `. Directory may be relative or absolute path. If setting relative path, then directory is created diff --git a/pyproject.toml b/pyproject.toml index aa57762e75d..493213d841e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,49 @@ requires = [ ] build-backend = "setuptools.build_meta" +[tool.pytest.ini_options] +minversion = "2.0" +addopts = "-rfEX -p pytester --strict-markers" +python_files = ["test_*.py", "*_test.py", "testing/*/*.py"] +python_classes = ["Test", "Acceptance"] +python_functions = ["test"] +# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting". +testpaths = ["testing"] +norecursedirs = ["testing/example_scripts"] +xfail_strict = true +filterwarnings = [ + "error", + "default:Using or importing the ABCs:DeprecationWarning:unittest2.*", + "default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*", + "ignore:Module already imported so cannot be rewritten:pytest.PytestWarning", + # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8)." + "ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest))", + # produced by pytest-xdist + "ignore:.*type argument to addoption.*:DeprecationWarning", + # produced by python >=3.5 on execnet (pytest-xdist) + "ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning", + # pytest's own futurewarnings + "ignore::pytest.PytestExperimentalApiWarning", + # Do not cause SyntaxError for invalid escape sequences in py37. + # Those are caught/handled by pyupgrade, and not easy to filter with the + # module being the filename (with .py removed). + "default:invalid escape sequence:DeprecationWarning", + # ignore use of unregistered marks, because we use many to test the implementation + "ignore::_pytest.warning_types.PytestUnknownMarkWarning", +] +pytester_example_dir = "testing/example_scripts" +markers = [ + # dummy markers for testing + "foo", + "bar", + "baz", + # conftest.py reorders tests moving slow ones to the end of the list + "slow", + # experimental mark for all tests using pexpect + "uses_pexpect", +] + + [tool.towncrier] package = "pytest" package_dir = "src" diff --git a/setup.cfg b/setup.cfg index 5dc778d9991..8749334f88c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ install_requires = packaging pluggy>=0.12,<1.0 py>=1.5.0 + toml atomicwrites>=1.0;sys_platform=="win32" colorama;sys_platform=="win32" importlib-metadata>=0.12;python_version<"3.8" diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 27083900dfa..4b1d0bd2afb 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -34,7 +34,6 @@ from .exceptions import PrintHelp from .exceptions import UsageError from .findpaths import determine_setup -from .findpaths import exists from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback from _pytest._io import TerminalWriter @@ -450,7 +449,7 @@ def _set_initial_conftests(self, namespace): if i != -1: path = path[:i] anchor = current.join(path, abs=1) - if exists(anchor): # we found some file object + if anchor.exists(): # we found some file object self._try_load_conftest(anchor) foundanchor = True if not foundanchor: @@ -1069,13 +1068,8 @@ def _checkversion(self): if Version(minver) > Version(pytest.__version__): raise pytest.UsageError( - "%s:%d: requires pytest-%s, actual pytest-%s'" - % ( - self.inicfg.config.path, - self.inicfg.lineof("minversion"), - minver, - pytest.__version__, - ) + "%s: 'minversion' requires pytest-%s, actual pytest-%s'" + % (self.inifile, minver, pytest.__version__,) ) def _validatekeys(self): @@ -1123,7 +1117,7 @@ def addinivalue_line(self, name, line): x.append(line) # modifies the cached list inline def getini(self, name: str): - """ return configuration value from an :ref:`ini file `. If the + """ return configuration value from an :ref:`ini file `. If the specified name hasn't been registered through a prior :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>` call (usually from a plugin), a ValueError is raised. """ @@ -1138,8 +1132,8 @@ def _getini(self, name: str) -> Any: description, type, default = self._parser._inidict[name] except KeyError: raise ValueError("unknown configuration value: {!r}".format(name)) - value = self._get_override_ini_value(name) - if value is None: + override_value = self._get_override_ini_value(name) + if override_value is None: try: value = self.inicfg[name] except KeyError: @@ -1148,18 +1142,35 @@ def _getini(self, name: str) -> Any: if type is None: return "" return [] + else: + value = override_value + # coerce the values based on types + # note: some coercions are only required if we are reading from .ini files, because + # the file format doesn't contain type information, but when reading from toml we will + # get either str or list of str values (see _parse_ini_config_from_pyproject_toml). + # for example: + # + # ini: + # a_line_list = "tests acceptance" + # in this case, we need to split the string to obtain a list of strings + # + # toml: + # a_line_list = ["tests", "acceptance"] + # in this case, we already have a list ready to use + # if type == "pathlist": - dp = py.path.local(self.inicfg.config.path).dirpath() - values = [] - for relpath in shlex.split(value): - values.append(dp.join(relpath, abs=True)) - return values + dp = py.path.local(self.inifile).dirpath() + input_values = shlex.split(value) if isinstance(value, str) else value + return [dp.join(x, abs=True) for x in input_values] elif type == "args": - return shlex.split(value) + return shlex.split(value) if isinstance(value, str) else value elif type == "linelist": - return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] + if isinstance(value, str): + return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] + else: + return value elif type == "bool": - return bool(_strtobool(value.strip())) + return bool(_strtobool(str(value).strip())) else: assert type is None return value diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 2b252c4f474..796fa9b0ae0 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -1,13 +1,13 @@ import os -from typing import Any +from typing import Dict from typing import Iterable from typing import List from typing import Optional from typing import Tuple +from typing import Union +import iniconfig import py -from iniconfig import IniConfig -from iniconfig import ParseError from .exceptions import UsageError from _pytest.compat import TYPE_CHECKING @@ -17,52 +17,95 @@ from . import Config -def exists(path, ignore=OSError): +def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig: + """Parses the given generic '.ini' file using legacy IniConfig parser, returning + the parsed object. + + Raises UsageError if the file cannot be parsed. + """ try: - return path.check() - except ignore: - return False + return iniconfig.IniConfig(path) + except iniconfig.ParseError as exc: + raise UsageError(str(exc)) + +def load_config_dict_from_file( + filepath: py.path.local, +) -> Optional[Dict[str, Union[str, List[str]]]]: + """Loads pytest configuration from the given file path, if supported. -def getcfg(args, config=None): + Return None if the file does not contain valid pytest configuration. """ - Search the list of arguments for a valid ini-file for pytest, - and return a tuple of (rootdir, inifile, cfg-dict). - note: config is optional and used only to issue warnings explicitly (#2891). + # configuration from ini files are obtained from the [pytest] section, if present. + if filepath.ext == ".ini": + iniconfig = _parse_ini_config(filepath) + + if "pytest" in iniconfig: + return dict(iniconfig["pytest"].items()) + else: + # "pytest.ini" files are always the source of configuration, even if empty + if filepath.basename == "pytest.ini": + return {} + + # '.cfg' files are considered if they contain a "[tool:pytest]" section + elif filepath.ext == ".cfg": + iniconfig = _parse_ini_config(filepath) + + if "tool:pytest" in iniconfig.sections: + return dict(iniconfig["tool:pytest"].items()) + elif "pytest" in iniconfig.sections: + # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that + # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). + fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) + + # '.toml' files are considered if they contain a [tool.pytest.ini_options] table + elif filepath.ext == ".toml": + import toml + + config = toml.load(filepath) + + result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) + if result is not None: + # TOML supports richer data types than ini files (strings, arrays, floats, ints, etc), + # however we need to convert all scalar values to str for compatibility with the rest + # of the configuration system, which expects strings only. + def make_scalar(v: object) -> Union[str, List[str]]: + return v if isinstance(v, list) else str(v) + + return {k: make_scalar(v) for k, v in result.items()} + + return None + + +def locate_config( + args: Iterable[Union[str, py.path.local]] +) -> Tuple[ + Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]], +]: + """ + Search in the list of arguments for a valid ini-file for pytest, + and return a tuple of (rootdir, inifile, cfg-dict). """ - inibasenames = ["pytest.ini", "tox.ini", "setup.cfg"] + config_names = [ + "pytest.ini", + "pyproject.toml", + "tox.ini", + "setup.cfg", + ] args = [x for x in args if not str(x).startswith("-")] if not args: args = [py.path.local()] for arg in args: arg = py.path.local(arg) for base in arg.parts(reverse=True): - for inibasename in inibasenames: - p = base.join(inibasename) - if exists(p): - try: - iniconfig = IniConfig(p) - except ParseError as exc: - raise UsageError(str(exc)) - - if ( - inibasename == "setup.cfg" - and "tool:pytest" in iniconfig.sections - ): - return base, p, iniconfig["tool:pytest"] - elif "pytest" in iniconfig.sections: - if inibasename == "setup.cfg" and config is not None: - - fail( - CFG_PYTEST_SECTION.format(filename=inibasename), - pytrace=False, - ) - return base, p, iniconfig["pytest"] - elif inibasename == "pytest.ini": - # allowed to be empty - return base, p, {} - return None, None, None + for config_name in config_names: + p = base.join(config_name) + if p.isfile(): + ini_config = load_config_dict_from_file(p) + if ini_config is not None: + return base, p, ini_config + return None, None, {} def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local: @@ -118,29 +161,16 @@ def determine_setup( args: List[str], rootdir_cmd_arg: Optional[str] = None, config: Optional["Config"] = None, -) -> Tuple[py.path.local, Optional[str], Any]: +) -> Tuple[py.path.local, Optional[str], Dict[str, Union[str, List[str]]]]: + rootdir = None dirs = get_dirs_from_args(args) if inifile: - iniconfig = IniConfig(inifile) - is_cfg_file = str(inifile).endswith(".cfg") - sections = ["tool:pytest", "pytest"] if is_cfg_file else ["pytest"] - for section in sections: - try: - inicfg = iniconfig[ - section - ] # type: Optional[py.iniconfig._SectionWrapper] - if is_cfg_file and section == "pytest" and config is not None: - fail( - CFG_PYTEST_SECTION.format(filename=str(inifile)), pytrace=False - ) - break - except KeyError: - inicfg = None + inicfg = load_config_dict_from_file(py.path.local(inifile)) or {} if rootdir_cmd_arg is None: rootdir = get_common_ancestor(dirs) else: ancestor = get_common_ancestor(dirs) - rootdir, inifile, inicfg = getcfg([ancestor], config=config) + rootdir, inifile, inicfg = locate_config([ancestor]) if rootdir is None and rootdir_cmd_arg is None: for possible_rootdir in ancestor.parts(reverse=True): if possible_rootdir.join("setup.py").exists(): @@ -148,7 +178,7 @@ def determine_setup( break else: if dirs != [ancestor]: - rootdir, inifile, inicfg = getcfg(dirs, config=config) + rootdir, inifile, inicfg = locate_config(dirs) if rootdir is None: if config is not None: cwd = config.invocation_dir diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 8df5992d659..2913c60654a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -688,6 +688,13 @@ def getinicfg(self, source): p = self.makeini(source) return IniConfig(p)["pytest"] + def makepyprojecttoml(self, source): + """Write a pyproject.toml file with 'source' as contents. + + .. versionadded:: 6.0 + """ + return self.makefile(".toml", pyproject=source) + def makepyfile(self, *args, **kwargs): r"""Shortcut for .makefile() with a .py extension. Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index b37828e5a5c..9c2665fb818 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -691,7 +691,7 @@ def pytest_report_header(self, config: Config) -> List[str]: line = "rootdir: %s" % config.rootdir if config.inifile: - line += ", inifile: " + config.rootdir.bestrelpath(config.inifile) + line += ", configfile: " + config.rootdir.bestrelpath(config.inifile) testpaths = config.getini("testpaths") if testpaths and config.args == testpaths: diff --git a/testing/test_config.py b/testing/test_config.py index 867012e932c..31dfd9fa30a 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -18,7 +18,7 @@ from _pytest.config.exceptions import UsageError from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor -from _pytest.config.findpaths import getcfg +from _pytest.config.findpaths import locate_config from _pytest.pathlib import Path @@ -39,14 +39,14 @@ def test_getcfg_and_config(self, testdir, tmpdir, section, filename): ) ) ) - _, _, cfg = getcfg([sub]) + _, _, cfg = locate_config([sub]) assert cfg["name"] == "value" config = testdir.parseconfigure(sub) assert config.inicfg["name"] == "value" def test_getcfg_empty_path(self): """correctly handle zero length arguments (a la pytest '')""" - getcfg([""]) + locate_config([""]) def test_setupcfg_uses_toolpytest_with_pytest(self, testdir): p1 = testdir.makepyfile("def test(): pass") @@ -61,7 +61,7 @@ def test_setupcfg_uses_toolpytest_with_pytest(self, testdir): % p1.basename, ) result = testdir.runpytest() - result.stdout.fnmatch_lines(["*, inifile: setup.cfg, *", "* 1 passed in *"]) + result.stdout.fnmatch_lines(["*, configfile: setup.cfg, *", "* 1 passed in *"]) assert result.ret == 0 def test_append_parse_args(self, testdir, tmpdir, monkeypatch): @@ -85,12 +85,14 @@ def test_tox_ini_wrong_version(self, testdir): ".ini", tox=""" [pytest] - minversion=9.0 + minversion=999.0 """, ) result = testdir.runpytest() assert result.ret != 0 - result.stderr.fnmatch_lines(["*tox.ini:2*requires*9.0*actual*"]) + result.stderr.fnmatch_lines( + ["*tox.ini: 'minversion' requires pytest-999.0, actual pytest-*"] + ) @pytest.mark.parametrize( "section, name", @@ -110,6 +112,16 @@ def test_ini_names(self, testdir, name, section): config = testdir.parseconfig() assert config.getini("minversion") == "1.0" + def test_pyproject_toml(self, testdir): + testdir.makepyprojecttoml( + """ + [tool.pytest.ini_options] + minversion = "1.0" + """ + ) + config = testdir.parseconfig() + assert config.getini("minversion") == "1.0" + def test_toxini_before_lower_pytestini(self, testdir): sub = testdir.tmpdir.mkdir("sub") sub.join("tox.ini").write( @@ -251,6 +263,18 @@ def pytest_addoption(parser): config = testdir.parseconfig("-c", "custom_tool_pytest_section.cfg") assert config.getini("custom") == "1" + testdir.makefile( + ".toml", + custom=""" + [tool.pytest.ini_options] + custom = 1 + value = [ + ] # this is here on purpose, as it makes this an invalid '.ini' file + """, + ) + config = testdir.parseconfig("-c", "custom.toml") + assert config.getini("custom") == "1" + def test_absolute_win32_path(self, testdir): temp_ini_file = testdir.makefile( ".ini", @@ -350,7 +374,7 @@ def pytest_addoption(parser): assert val == "hello" pytest.raises(ValueError, config.getini, "other") - def test_addini_pathlist(self, testdir): + def make_conftest_for_pathlist(self, testdir): testdir.makeconftest( """ def pytest_addoption(parser): @@ -358,20 +382,36 @@ def pytest_addoption(parser): parser.addini("abc", "abc value") """ ) + + def test_addini_pathlist_ini_files(self, testdir): + self.make_conftest_for_pathlist(testdir) p = testdir.makeini( """ [pytest] paths=hello world/sub.py """ ) + self.check_config_pathlist(testdir, p) + + def test_addini_pathlist_pyproject_toml(self, testdir): + self.make_conftest_for_pathlist(testdir) + p = testdir.makepyprojecttoml( + """ + [tool.pytest.ini_options] + paths=["hello", "world/sub.py"] + """ + ) + self.check_config_pathlist(testdir, p) + + def check_config_pathlist(self, testdir, config_path): config = testdir.parseconfig() values = config.getini("paths") assert len(values) == 2 - assert values[0] == p.dirpath("hello") - assert values[1] == p.dirpath("world/sub.py") + assert values[0] == config_path.dirpath("hello") + assert values[1] == config_path.dirpath("world/sub.py") pytest.raises(ValueError, config.getini, "other") - def test_addini_args(self, testdir): + def make_conftest_for_args(self, testdir): testdir.makeconftest( """ def pytest_addoption(parser): @@ -379,20 +419,35 @@ def pytest_addoption(parser): parser.addini("a2", "", "args", default="1 2 3".split()) """ ) + + def test_addini_args_ini_files(self, testdir): + self.make_conftest_for_args(testdir) testdir.makeini( """ [pytest] args=123 "123 hello" "this" - """ + """ ) + self.check_config_args(testdir) + + def test_addini_args_pyproject_toml(self, testdir): + self.make_conftest_for_args(testdir) + testdir.makepyprojecttoml( + """ + [tool.pytest.ini_options] + args = ["123", "123 hello", "this"] + """ + ) + self.check_config_args(testdir) + + def check_config_args(self, testdir): config = testdir.parseconfig() values = config.getini("args") - assert len(values) == 3 assert values == ["123", "123 hello", "this"] values = config.getini("a2") assert values == list("123") - def test_addini_linelist(self, testdir): + def make_conftest_for_linelist(self, testdir): testdir.makeconftest( """ def pytest_addoption(parser): @@ -400,6 +455,9 @@ def pytest_addoption(parser): parser.addini("a2", "", "linelist") """ ) + + def test_addini_linelist_ini_files(self, testdir): + self.make_conftest_for_linelist(testdir) testdir.makeini( """ [pytest] @@ -407,6 +465,19 @@ def pytest_addoption(parser): second line """ ) + self.check_config_linelist(testdir) + + def test_addini_linelist_pprojecttoml(self, testdir): + self.make_conftest_for_linelist(testdir) + testdir.makepyprojecttoml( + """ + [tool.pytest.ini_options] + xy = ["123 345", "second line"] + """ + ) + self.check_config_linelist(testdir) + + def check_config_linelist(self, testdir): config = testdir.parseconfig() values = config.getini("xy") assert len(values) == 2 @@ -832,7 +903,6 @@ def test_consider_args_after_options_for_rootdir(testdir, args): result.stdout.fnmatch_lines(["*rootdir: *myroot"]) -@pytest.mark.skipif("sys.platform == 'win32'") def test_toolongargs_issue224(testdir): result = testdir.runpytest("-m", "hello" * 500) assert result.ret == ExitCode.NO_TESTS_COLLECTED @@ -964,10 +1034,20 @@ def test_simple_noini(self, tmpdir): assert get_common_ancestor([no_path]) == tmpdir assert get_common_ancestor([no_path.join("a")]) == tmpdir - @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) - def test_with_ini(self, tmpdir: py.path.local, name: str) -> None: + @pytest.mark.parametrize( + "name, contents", + [ + pytest.param("pytest.ini", "[pytest]\nx=10", id="pytest.ini"), + pytest.param( + "pyproject.toml", "[tool.pytest.ini_options]\nx=10", id="pyproject.toml" + ), + pytest.param("tox.ini", "[pytest]\nx=10", id="tox.ini"), + pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"), + ], + ) + def test_with_ini(self, tmpdir: py.path.local, name: str, contents: str) -> None: inifile = tmpdir.join(name) - inifile.write("[pytest]\n" if name != "setup.cfg" else "[tool:pytest]\n") + inifile.write(contents) a = tmpdir.mkdir("a") b = a.mkdir("b") @@ -975,9 +1055,10 @@ def test_with_ini(self, tmpdir: py.path.local, name: str) -> None: rootdir, parsed_inifile, _ = determine_setup(None, args) assert rootdir == tmpdir assert parsed_inifile == inifile - rootdir, parsed_inifile, _ = determine_setup(None, [str(b), str(a)]) + rootdir, parsed_inifile, ini_config = determine_setup(None, [str(b), str(a)]) assert rootdir == tmpdir assert parsed_inifile == inifile + assert ini_config == {"x": "10"} @pytest.mark.parametrize("name", "setup.cfg tox.ini".split()) def test_pytestini_overrides_empty_other(self, tmpdir: py.path.local, name) -> None: @@ -1004,10 +1085,26 @@ def test_nothing(self, tmpdir: py.path.local, monkeypatch) -> None: assert inifile is None assert inicfg == {} - def test_with_specific_inifile(self, tmpdir: py.path.local) -> None: - inifile = tmpdir.ensure("pytest.ini") - rootdir, _, _ = determine_setup(str(inifile), [str(tmpdir)]) + @pytest.mark.parametrize( + "name, contents", + [ + # pytest.param("pytest.ini", "[pytest]\nx=10", id="pytest.ini"), + pytest.param( + "pyproject.toml", "[tool.pytest.ini_options]\nx=10", id="pyproject.toml" + ), + # pytest.param("tox.ini", "[pytest]\nx=10", id="tox.ini"), + # pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"), + ], + ) + def test_with_specific_inifile( + self, tmpdir: py.path.local, name: str, contents: str + ) -> None: + p = tmpdir.ensure(name) + p.write(contents) + rootdir, inifile, ini_config = determine_setup(str(p), [str(tmpdir)]) assert rootdir == tmpdir + assert inifile == p + assert ini_config == {"x": "10"} def test_with_arg_outside_cwd_without_inifile(self, tmpdir, monkeypatch) -> None: monkeypatch.chdir(str(tmpdir)) diff --git a/testing/test_findpaths.py b/testing/test_findpaths.py new file mode 100644 index 00000000000..3de2ea21828 --- /dev/null +++ b/testing/test_findpaths.py @@ -0,0 +1,110 @@ +from textwrap import dedent + +import py + +import pytest +from _pytest.config.findpaths import get_common_ancestor +from _pytest.config.findpaths import load_config_dict_from_file + + +class TestLoadConfigDictFromFile: + def test_empty_pytest_ini(self, tmpdir): + """pytest.ini files are always considered for configuration, even if empty""" + fn = tmpdir.join("pytest.ini") + fn.write("") + assert load_config_dict_from_file(fn) == {} + + def test_pytest_ini(self, tmpdir): + """[pytest] section in pytest.ini files is read correctly""" + fn = tmpdir.join("pytest.ini") + fn.write("[pytest]\nx=1") + assert load_config_dict_from_file(fn) == {"x": "1"} + + def test_custom_ini(self, tmpdir): + """[pytest] section in any .ini file is read correctly""" + fn = tmpdir.join("custom.ini") + fn.write("[pytest]\nx=1") + assert load_config_dict_from_file(fn) == {"x": "1"} + + def test_custom_ini_without_section(self, tmpdir): + """Custom .ini files without [pytest] section are not considered for configuration""" + fn = tmpdir.join("custom.ini") + fn.write("[custom]") + assert load_config_dict_from_file(fn) is None + + def test_custom_cfg_file(self, tmpdir): + """Custom .cfg files without [tool:pytest] section are not considered for configuration""" + fn = tmpdir.join("custom.cfg") + fn.write("[custom]") + assert load_config_dict_from_file(fn) is None + + def test_valid_cfg_file(self, tmpdir): + """Custom .cfg files with [tool:pytest] section are read correctly""" + fn = tmpdir.join("custom.cfg") + fn.write("[tool:pytest]\nx=1") + assert load_config_dict_from_file(fn) == {"x": "1"} + + def test_unsupported_pytest_section_in_cfg_file(self, tmpdir): + """.cfg files with [pytest] section are no longer supported and should fail to alert users""" + fn = tmpdir.join("custom.cfg") + fn.write("[pytest]") + with pytest.raises(pytest.fail.Exception): + load_config_dict_from_file(fn) + + def test_invalid_toml_file(self, tmpdir): + """.toml files without [tool.pytest.ini_options] are not considered for configuration.""" + fn = tmpdir.join("myconfig.toml") + fn.write( + dedent( + """ + [build_system] + x = 1 + """ + ) + ) + assert load_config_dict_from_file(fn) is None + + def test_valid_toml_file(self, tmpdir): + """.toml files with [tool.pytest.ini_options] are read correctly, including changing + data types to str/list for compatibility with other configuration options.""" + fn = tmpdir.join("myconfig.toml") + fn.write( + dedent( + """ + [tool.pytest.ini_options] + x = 1 + y = 20.0 + values = ["tests", "integration"] + name = "foo" + """ + ) + ) + assert load_config_dict_from_file(fn) == { + "x": "1", + "y": "20.0", + "values": ["tests", "integration"], + "name": "foo", + } + + +class TestCommonAncestor: + def test_has_ancestor(self, tmpdir): + fn1 = tmpdir.join("foo/bar/test_1.py").ensure(file=1) + fn2 = tmpdir.join("foo/zaz/test_2.py").ensure(file=1) + assert get_common_ancestor([fn1, fn2]) == tmpdir.join("foo") + assert get_common_ancestor([py.path.local(fn1.dirname), fn2]) == tmpdir.join( + "foo" + ) + assert get_common_ancestor( + [py.path.local(fn1.dirname), py.path.local(fn2.dirname)] + ) == tmpdir.join("foo") + assert get_common_ancestor([fn1, py.path.local(fn2.dirname)]) == tmpdir.join( + "foo" + ) + + def test_single_dir(self, tmpdir): + assert get_common_ancestor([tmpdir]) == tmpdir + + def test_single_file(self, tmpdir): + fn = tmpdir.join("foo.py").ensure(file=1) + assert get_common_ancestor([fn]) == tmpdir diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 7d7c82ad68e..e8402079b6c 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -706,10 +706,10 @@ def test_header(self, testdir): result = testdir.runpytest() result.stdout.fnmatch_lines(["rootdir: *test_header0"]) - # with inifile + # with configfile testdir.makeini("""[pytest]""") result = testdir.runpytest() - result.stdout.fnmatch_lines(["rootdir: *test_header0, inifile: tox.ini"]) + result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"]) # with testpaths option, and not passing anything in the command-line testdir.makeini( @@ -720,12 +720,12 @@ def test_header(self, testdir): ) result = testdir.runpytest() result.stdout.fnmatch_lines( - ["rootdir: *test_header0, inifile: tox.ini, testpaths: tests, gui"] + ["rootdir: *test_header0, configfile: tox.ini, testpaths: tests, gui"] ) # with testpaths option, passing directory in command-line: do not show testpaths then result = testdir.runpytest("tests") - result.stdout.fnmatch_lines(["rootdir: *test_header0, inifile: tox.ini"]) + result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"]) def test_showlocals(self, testdir): p1 = testdir.makepyfile( diff --git a/tox.ini b/tox.ini index 8e1a51ca760..affb4a7a92c 100644 --- a/tox.ini +++ b/tox.ini @@ -152,48 +152,6 @@ deps = pypandoc commands = python scripts/publish-gh-release-notes.py {posargs} - -[pytest] -minversion = 2.0 -addopts = -rfEX -p pytester --strict-markers -rsyncdirs = tox.ini doc src testing -python_files = test_*.py *_test.py testing/*/*.py -python_classes = Test Acceptance -python_functions = test -# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting". -testpaths = testing -norecursedirs = testing/example_scripts -xfail_strict=true -filterwarnings = - error - default:Using or importing the ABCs:DeprecationWarning:unittest2.* - default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.* - ignore:Module already imported so cannot be rewritten:pytest.PytestWarning - # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8). - ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest)) - # produced by pytest-xdist - ignore:.*type argument to addoption.*:DeprecationWarning - # produced by python >=3.5 on execnet (pytest-xdist) - ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning - # pytest's own futurewarnings - ignore::pytest.PytestExperimentalApiWarning - # Do not cause SyntaxError for invalid escape sequences in py37. - # Those are caught/handled by pyupgrade, and not easy to filter with the - # module being the filename (with .py removed). - default:invalid escape sequence:DeprecationWarning - # ignore use of unregistered marks, because we use many to test the implementation - ignore::_pytest.warning_types.PytestUnknownMarkWarning -pytester_example_dir = testing/example_scripts -markers = - # dummy markers for testing - foo - bar - baz - # conftest.py reorders tests moving slow ones to the end of the list - slow - # experimental mark for all tests using pexpect - uses_pexpect - [flake8] max-line-length = 120 extend-ignore = E203 From 322190fd84e1b86d7b9a2d71f086445ca80c39b3 Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Mon, 8 Jun 2020 10:56:40 -0300 Subject: [PATCH 336/823] Fix issue where working dir becomes wrong on subst drive on Windows. Fixes #5965 (#6523) Co-authored-by: Bruno Oliveira --- changelog/5965.breaking.rst | 9 ++++ src/_pytest/capture.py | 6 +-- src/_pytest/config/__init__.py | 6 +-- src/_pytest/fixtures.py | 2 +- src/_pytest/main.py | 1 - src/_pytest/pathlib.py | 9 ++++ testing/acceptance_test.py | 74 +++++++---------------------- testing/test_collection.py | 32 ++++--------- testing/test_conftest.py | 56 ++++++++++------------ testing/test_link_resolve.py | 85 ++++++++++++++++++++++++++++++++++ 10 files changed, 160 insertions(+), 120 deletions(-) create mode 100644 changelog/5965.breaking.rst create mode 100644 testing/test_link_resolve.py diff --git a/changelog/5965.breaking.rst b/changelog/5965.breaking.rst new file mode 100644 index 00000000000..3ecb9486aed --- /dev/null +++ b/changelog/5965.breaking.rst @@ -0,0 +1,9 @@ +symlinks are no longer resolved during collection and matching `conftest.py` files with test file paths. + +Resolving symlinks for the current directory and during collection was introduced as a bugfix in 3.9.0, but it actually is a new feature which had unfortunate consequences in Windows and surprising results in other platforms. + +The team decided to step back on resolving symlinks at all, planning to review this in the future with a more solid solution (see discussion in +`#6523 `__ for details). + +This might break test suites which made use of this feature; the fix is to create a symlink +for the entire test tree, and not only to partial files/tress as it was possible previously. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 98ba878b3f0..04104128456 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -123,9 +123,9 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: return buffered = hasattr(stream.buffer, "raw") - raw_stdout = stream.buffer.raw if buffered else stream.buffer + raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined] - if not isinstance(raw_stdout, io._WindowsConsoleIO): + if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined] return def _reopen_stdio(f, mode): @@ -135,7 +135,7 @@ def _reopen_stdio(f, mode): buffering = -1 return io.TextIOWrapper( - open(os.dup(f.fileno()), mode, buffering), + open(os.dup(f.fileno()), mode, buffering), # type: ignore[arg-type] f.encoding, f.errors, f.newlines, diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4b1d0bd2afb..c94ea2a9319 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -232,7 +232,7 @@ def get_config(args=None, plugins=None): config = Config( pluginmanager, invocation_params=Config.InvocationParams( - args=args or (), plugins=plugins, dir=Path().resolve() + args=args or (), plugins=plugins, dir=Path.cwd() ), ) @@ -477,7 +477,7 @@ def _getconftestmodules(self, path): # and allow users to opt into looking into the rootdir parent # directories instead of requiring to specify confcutdir clist = [] - for parent in directory.realpath().parts(): + for parent in directory.parts(): if self._confcutdir and self._confcutdir.relto(parent): continue conftestpath = parent.join("conftest.py") @@ -798,7 +798,7 @@ def __init__( if invocation_params is None: invocation_params = self.InvocationParams( - args=(), plugins=None, dir=Path().resolve() + args=(), plugins=None, dir=Path.cwd() ) self.option = argparse.Namespace() diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index fa7e3e1df16..05f0ecb6a47 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1496,7 +1496,7 @@ def getfixtureinfo( def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: nodeid = None try: - p = py.path.local(plugin.__file__).realpath() # type: ignore[attr-defined] # noqa: F821 + p = py.path.local(plugin.__file__) # type: ignore[attr-defined] # noqa: F821 except AttributeError: pass else: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 1c1cda18bdf..84ee008812f 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -665,7 +665,6 @@ def _parsearg(self, arg: str) -> Tuple[py.path.local, List[str]]: "file or package not found: " + arg + " (missing __init__.py?)" ) raise UsageError("file not found: " + arg) - fspath = fspath.realpath() return (fspath, parts) def matchnodes( diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 6878965e0c5..69f490a1d48 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -18,6 +18,7 @@ from typing import TypeVar from typing import Union +from _pytest.outcomes import skip from _pytest.warning_types import PytestWarning if sys.version_info[:2] >= (3, 6): @@ -397,3 +398,11 @@ def fnmatch_ex(pattern: str, path) -> bool: def parts(s: str) -> Set[str]: parts = s.split(sep) return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} + + +def symlink_or_skip(src, dst, **kwargs): + """Makes a symlink or skips the test in case symlinks are not supported.""" + try: + os.symlink(str(src), str(dst), **kwargs) + except OSError as e: + skip("symlinks not supported: {}".format(e)) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index e2df92d8091..7dfd588a02b 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,6 +1,5 @@ import os import sys -import textwrap import types import attr @@ -9,6 +8,7 @@ import pytest from _pytest.compat import importlib_metadata from _pytest.config import ExitCode +from _pytest.pathlib import symlink_or_skip from _pytest.pytester import Testdir @@ -266,29 +266,6 @@ def test_conftest_printing_shows_if_error(self, testdir): assert result.ret != 0 assert "should be seen" in result.stdout.str() - @pytest.mark.skipif( - not hasattr(py.path.local, "mksymlinkto"), - reason="symlink not available on this platform", - ) - def test_chdir(self, testdir): - testdir.tmpdir.join("py").mksymlinkto(py._pydir) - p = testdir.tmpdir.join("main.py") - p.write( - textwrap.dedent( - """\ - import sys, os - sys.path.insert(0, '') - import py - print(py.__file__) - print(py.__path__) - os.chdir(os.path.dirname(os.getcwd())) - print(py.log) - """ - ) - ) - result = testdir.runpython(p) - assert not result.ret - def test_issue109_sibling_conftests_not_loaded(self, testdir): sub1 = testdir.mkdir("sub1") sub2 = testdir.mkdir("sub2") @@ -762,19 +739,9 @@ def test(): def test_cmdline_python_package_symlink(self, testdir, monkeypatch): """ - test --pyargs option with packages with path containing symlink can - have conftest.py in their package (#2985) + --pyargs with packages with path containing symlink can have conftest.py in + their package (#2985) """ - # dummy check that we can actually create symlinks: on Windows `os.symlink` is available, - # but normal users require special admin privileges to create symlinks. - if sys.platform == "win32": - try: - os.symlink( - str(testdir.tmpdir.ensure("tmpfile")), - str(testdir.tmpdir.join("tmpfile2")), - ) - except OSError as e: - pytest.skip(str(e.args[0])) monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) dirname = "lib" @@ -790,13 +757,13 @@ def test_cmdline_python_package_symlink(self, testdir, monkeypatch): "import pytest\n@pytest.fixture\ndef a_fixture():pass" ) - d_local = testdir.mkdir("local") - symlink_location = os.path.join(str(d_local), "lib") - os.symlink(str(d), symlink_location, target_is_directory=True) + d_local = testdir.mkdir("symlink_root") + symlink_location = d_local / "lib" + symlink_or_skip(d, symlink_location, target_is_directory=True) # The structure of the test directory is now: # . - # ├── local + # ├── symlink_root # │ └── lib -> ../lib # └── lib # └── foo @@ -807,32 +774,23 @@ def test_cmdline_python_package_symlink(self, testdir, monkeypatch): # └── test_bar.py # NOTE: the different/reversed ordering is intentional here. - search_path = ["lib", os.path.join("local", "lib")] + search_path = ["lib", os.path.join("symlink_root", "lib")] monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path)) for p in search_path: monkeypatch.syspath_prepend(p) # module picked up in symlink-ed directory: - # It picks up local/lib/foo/bar (symlink) via sys.path. + # It picks up symlink_root/lib/foo/bar (symlink) via sys.path. result = testdir.runpytest("--pyargs", "-v", "foo.bar") testdir.chdir() assert result.ret == 0 - if hasattr(py.path.local, "mksymlinkto"): - result.stdout.fnmatch_lines( - [ - "lib/foo/bar/test_bar.py::test_bar PASSED*", - "lib/foo/bar/test_bar.py::test_other PASSED*", - "*2 passed*", - ] - ) - else: - result.stdout.fnmatch_lines( - [ - "*lib/foo/bar/test_bar.py::test_bar PASSED*", - "*lib/foo/bar/test_bar.py::test_other PASSED*", - "*2 passed*", - ] - ) + result.stdout.fnmatch_lines( + [ + "symlink_root/lib/foo/bar/test_bar.py::test_bar PASSED*", + "symlink_root/lib/foo/bar/test_bar.py::test_other PASSED*", + "*2 passed*", + ] + ) def test_cmdline_python_package_not_exists(self, testdir): result = testdir.runpytest("--pyargs", "tpkgwhatv") diff --git a/testing/test_collection.py b/testing/test_collection.py index 8e5d5aaccb0..6644881ea44 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -3,12 +3,11 @@ import sys import textwrap -import py - import pytest from _pytest.config import ExitCode from _pytest.main import _in_venv from _pytest.main import Session +from _pytest.pathlib import symlink_or_skip from _pytest.pytester import Testdir @@ -1164,29 +1163,21 @@ def test_collect_pyargs_with_testpaths(testdir, monkeypatch): result.stdout.fnmatch_lines(["*1 passed in*"]) -@pytest.mark.skipif( - not hasattr(py.path.local, "mksymlinkto"), - reason="symlink not available on this platform", -) def test_collect_symlink_file_arg(testdir): - """Test that collecting a direct symlink, where the target does not match python_files works (#4325).""" + """Collect a direct symlink works even if it does not match python_files (#4325).""" real = testdir.makepyfile( real=""" def test_nodeid(request): - assert request.node.nodeid == "real.py::test_nodeid" + assert request.node.nodeid == "symlink.py::test_nodeid" """ ) symlink = testdir.tmpdir.join("symlink.py") - symlink.mksymlinkto(real) + symlink_or_skip(real, symlink) result = testdir.runpytest("-v", symlink) - result.stdout.fnmatch_lines(["real.py::test_nodeid PASSED*", "*1 passed in*"]) + result.stdout.fnmatch_lines(["symlink.py::test_nodeid PASSED*", "*1 passed in*"]) assert result.ret == 0 -@pytest.mark.skipif( - not hasattr(py.path.local, "mksymlinkto"), - reason="symlink not available on this platform", -) def test_collect_symlink_out_of_tree(testdir): """Test collection of symlink via out-of-tree rootdir.""" sub = testdir.tmpdir.join("sub") @@ -1204,7 +1195,7 @@ def test_nodeid(request): out_of_tree = testdir.tmpdir.join("out_of_tree").ensure(dir=True) symlink_to_sub = out_of_tree.join("symlink_to_sub") - symlink_to_sub.mksymlinkto(sub) + symlink_or_skip(sub, symlink_to_sub) sub.chdir() result = testdir.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub) result.stdout.fnmatch_lines( @@ -1270,22 +1261,19 @@ def test_collect_pkg_init_only(testdir): result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) -@pytest.mark.skipif( - not hasattr(py.path.local, "mksymlinkto"), - reason="symlink not available on this platform", -) @pytest.mark.parametrize("use_pkg", (True, False)) def test_collect_sub_with_symlinks(use_pkg, testdir): + """Collection works with symlinked files and broken symlinks""" sub = testdir.mkdir("sub") if use_pkg: sub.ensure("__init__.py") - sub.ensure("test_file.py").write("def test_file(): pass") + sub.join("test_file.py").write("def test_file(): pass") # Create a broken symlink. - sub.join("test_broken.py").mksymlinkto("test_doesnotexist.py") + symlink_or_skip("test_doesnotexist.py", sub.join("test_broken.py")) # Symlink that gets collected. - sub.join("test_symlink.py").mksymlinkto("test_file.py") + symlink_or_skip("test_file.py", sub.join("test_symlink.py")) result = testdir.runpytest("-v", str(sub)) result.stdout.fnmatch_lines( diff --git a/testing/test_conftest.py b/testing/test_conftest.py index a07af60f6f9..0df303bc7cb 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -7,6 +7,7 @@ from _pytest.config import ExitCode from _pytest.config import PytestPluginManager from _pytest.pathlib import Path +from _pytest.pathlib import symlink_or_skip def ConftestWithSetinitial(path): @@ -190,16 +191,25 @@ def pytest_addoption(parser): result.stdout.no_fnmatch_line("*warning: could not load initial*") -@pytest.mark.skipif( - not hasattr(py.path.local, "mksymlinkto"), - reason="symlink not available on this platform", -) def test_conftest_symlink(testdir): - """Ensure that conftest.py is used for resolved symlinks.""" + """ + conftest.py discovery follows normal path resolution and does not resolve symlinks. + """ + # Structure: + # /real + # /real/conftest.py + # /real/app + # /real/app/tests + # /real/app/tests/test_foo.py + + # Links: + # /symlinktests -> /real/app/tests (running at symlinktests should fail) + # /symlink -> /real (running at /symlink should work) + real = testdir.tmpdir.mkdir("real") realtests = real.mkdir("app").mkdir("tests") - testdir.tmpdir.join("symlinktests").mksymlinkto(realtests) - testdir.tmpdir.join("symlink").mksymlinkto(real) + symlink_or_skip(realtests, testdir.tmpdir.join("symlinktests")) + symlink_or_skip(real, testdir.tmpdir.join("symlink")) testdir.makepyfile( **{ "real/app/tests/test_foo.py": "def test1(fixture): pass", @@ -216,38 +226,20 @@ def fixture(): ), } ) + + # Should fail because conftest cannot be found from the link structure. result = testdir.runpytest("-vs", "symlinktests") - result.stdout.fnmatch_lines( - [ - "*conftest_loaded*", - "real/app/tests/test_foo.py::test1 fixture_used", - "PASSED", - ] - ) - assert result.ret == ExitCode.OK + result.stdout.fnmatch_lines(["*fixture 'fixture' not found*"]) + assert result.ret == ExitCode.TESTS_FAILED # Should not cause "ValueError: Plugin already registered" (#4174). result = testdir.runpytest("-vs", "symlink") assert result.ret == ExitCode.OK - realtests.ensure("__init__.py") - result = testdir.runpytest("-vs", "symlinktests/test_foo.py::test1") - result.stdout.fnmatch_lines( - [ - "*conftest_loaded*", - "real/app/tests/test_foo.py::test1 fixture_used", - "PASSED", - ] - ) - assert result.ret == ExitCode.OK - -@pytest.mark.skipif( - not hasattr(py.path.local, "mksymlinkto"), - reason="symlink not available on this platform", -) def test_conftest_symlink_files(testdir): - """Check conftest.py loading when running in directory with symlinks.""" + """Symlinked conftest.py are found when pytest is executed in a directory with symlinked + files.""" real = testdir.tmpdir.mkdir("real") source = { "app/test_foo.py": "def test1(fixture): pass", @@ -271,7 +263,7 @@ def fixture(): build = testdir.tmpdir.mkdir("build") build.mkdir("app") for f in source: - build.join(f).mksymlinkto(real.join(f)) + symlink_or_skip(real.join(f), build.join(f)) build.chdir() result = testdir.runpytest("-vs", "app/test_foo.py") result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"]) diff --git a/testing/test_link_resolve.py b/testing/test_link_resolve.py new file mode 100644 index 00000000000..3e9199dff56 --- /dev/null +++ b/testing/test_link_resolve.py @@ -0,0 +1,85 @@ +import os.path +import subprocess +import sys +import textwrap +from contextlib import contextmanager +from string import ascii_lowercase + +import py.path + +from _pytest import pytester + + +@contextmanager +def subst_path_windows(filename): + for c in ascii_lowercase[7:]: # Create a subst drive from H-Z. + c += ":" + if not os.path.exists(c): + drive = c + break + else: + raise AssertionError("Unable to find suitable drive letter for subst.") + + directory = filename.dirpath() + basename = filename.basename + + args = ["subst", drive, str(directory)] + subprocess.check_call(args) + assert os.path.exists(drive) + try: + filename = py.path.local(drive) / basename + yield filename + finally: + args = ["subst", "/D", drive] + subprocess.check_call(args) + + +@contextmanager +def subst_path_linux(filename): + directory = filename.dirpath() + basename = filename.basename + + target = directory / ".." / "sub2" + os.symlink(str(directory), str(target), target_is_directory=True) + try: + filename = target / basename + yield filename + finally: + # We don't need to unlink (it's all in the tempdir). + pass + + +def test_link_resolve(testdir: pytester.Testdir) -> None: + """ + See: https://github.com/pytest-dev/pytest/issues/5965 + """ + sub1 = testdir.mkpydir("sub1") + p = sub1.join("test_foo.py") + p.write( + textwrap.dedent( + """ + import pytest + def test_foo(): + raise AssertionError() + """ + ) + ) + + subst = subst_path_linux + if sys.platform == "win32": + subst = subst_path_windows + + with subst(p) as subst_p: + result = testdir.runpytest(str(subst_p), "-v") + # i.e.: Make sure that the error is reported as a relative path, not as a + # resolved path. + # See: https://github.com/pytest-dev/pytest/issues/5965 + stdout = result.stdout.str() + assert "sub1/test_foo.py" not in stdout + + # i.e.: Expect drive on windows because we just have drive:filename, whereas + # we expect a relative path on Linux. + expect = ( + "*{}*".format(subst_p) if sys.platform == "win32" else "*sub2/test_foo.py*" + ) + result.stdout.fnmatch_lines([expect]) From a76855912b599d53865c9019b10ae934875fbe04 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 8 Jun 2020 21:15:53 -0300 Subject: [PATCH 337/823] Introduce guidelines for closing stale issues/PRs (#7332) * Introduce guidelines for closing stale issues/PRs Close #7282 Co-authored-by: Anthony Sottile Co-authored-by: Zac Hatfield-Dodds Co-authored-by: Anthony Sottile Co-authored-by: Zac Hatfield-Dodds --- CONTRIBUTING.rst | 71 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 3d07db63804..d5bd7814417 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -289,7 +289,7 @@ Here is a simple overview, with pytest-specific bits: Writing Tests ----------------------------- +~~~~~~~~~~~~~ Writing tests for plugins or for pytest itself is often done using the `testdir fixture `_, as a "black-box" test. @@ -328,6 +328,19 @@ one file which looks like a good fit. For example, a regression test about a bug should go into ``test_cacheprovider.py``, given that this option is implemented in ``cacheprovider.py``. If in doubt, go ahead and open a PR with your best guess and we can discuss this over the code. +Joining the Development Team +---------------------------- + +Anyone who has successfully seen through a pull request which did not +require any extra work from the development team to merge will +themselves gain commit access if they so wish (if we forget to ask please send a friendly +reminder). This does not mean there is any change in your contribution workflow: +everyone goes through the same pull-request-and-review process and +no-one merges their own pull requests unless already approved. It does however mean you can +participate in the development process more fully since you can merge +pull requests from other contributors yourself after having reviewed +them. + Backporting bug fixes for the next patch release ------------------------------------------------ @@ -359,15 +372,49 @@ actual latest release). The procedure for this is: * Delete the PR body, it usually contains a duplicate commit message. -Joining the Development Team ----------------------------- +Handling stale issues/PRs +------------------------- -Anyone who has successfully seen through a pull request which did not -require any extra work from the development team to merge will -themselves gain commit access if they so wish (if we forget to ask please send a friendly -reminder). This does not mean there is any change in your contribution workflow: -everyone goes through the same pull-request-and-review process and -no-one merges their own pull requests unless already approved. It does however mean you can -participate in the development process more fully since you can merge -pull requests from other contributors yourself after having reviewed -them. +Stale issues/PRs are those where pytest contributors have asked for questions/changes +and the authors didn't get around to answer/implement them yet after a somewhat long time, or +the discussion simply died because people seemed to lose interest. + +There are many reasons why people don't answer questions or implement requested changes: +they might get busy, lose interest, or just forget about it, +but the fact is that this is very common in open source software. + +The pytest team really appreciates every issue and pull request, but being a high-volume project +with many issues and pull requests being submitted daily, we try to reduce the number of stale +issues and PRs by regularly closing them. When an issue/pull request is closed in this manner, +it is by no means a dismissal of the topic being tackled by the issue/pull request, but it +is just a way for us to clear up the queue and make the maintainers' work more manageable. Submitters +can always reopen the issue/pull request in their own time later if it makes sense. + +When to close +~~~~~~~~~~~~~ + +Here are a few general rules the maintainers use to decide when to close issues/PRs because +of lack of inactivity: + +* Issues labeled ``question`` or ``needs information``: closed after 14 days inactive. +* Issues labeled ``proposal``: closed after six months inactive. +* Pull requests: after one month, consider pinging the author, update linked issue, or consider closing. For pull requests which are nearly finished, the team should consider finishing it up and merging it. + +The above are **not hard rules**, but merely **guidelines**, and can be (and often are!) reviewed on a case-by-case basis. + +Closing pull requests +~~~~~~~~~~~~~~~~~~~~~ + +When closing a Pull Request, it needs to be acknowledge the time, effort, and interest demonstrated by the person which submitted it. As mentioned previously, it is not the intent of the team to dismiss stalled pull request entirely but to merely to clear up our queue, so a message like the one below is warranted when closing a pull request that went stale: + + Hi , + + First of all we would like to thank you for your time and effort on working on this, the pytest team deeply appreciates it. + + We noticed it has been awhile since you have updated this PR, however. pytest is a high activity project, with many issues/PRs being opened daily, so it is hard for us maintainers to track which PRs are ready for merging, for review, or need more attention. + + So for those reasons we think it is best to close the PR for now, but with the only intention to cleanup our queue, it is by no means a rejection of your changes. We still encourage you to re-open this PR (it is just a click of a button away) when you are ready to get back to it. + + Again we appreciate your time for working on this, and hope you might get back to this at a later time! + + From e78207c936c43478aa5d5531d7c0b90aa240c9e0 Mon Sep 17 00:00:00 2001 From: Prashant Anand Date: Tue, 9 Jun 2020 09:54:22 +0900 Subject: [PATCH 338/823] 7119: data loss with mistyped --basetemp (#7170) Co-authored-by: Bruno Oliveira Co-authored-by: Ran Benita --- AUTHORS | 1 + changelog/7119.improvement.rst | 2 ++ src/_pytest/main.py | 31 +++++++++++++++++++++++++++++++ testing/test_main.py | 23 +++++++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 changelog/7119.improvement.rst diff --git a/AUTHORS b/AUTHORS index e1b195b9a88..4c5ca41af4b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -227,6 +227,7 @@ Pedro Algarvio Philipp Loose Pieter Mulder Piotr Banaszkiewicz +Prashant Anand Pulkit Goyal Punyashloka Biswal Quentin Pradet diff --git a/changelog/7119.improvement.rst b/changelog/7119.improvement.rst new file mode 100644 index 00000000000..6cef9883633 --- /dev/null +++ b/changelog/7119.improvement.rst @@ -0,0 +1,2 @@ +Exit with an error if the ``--basetemp`` argument is empty, the current working directory or parent directory of it. +This is done to protect against accidental data loss, as any directory passed to this argument is cleared. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 84ee008812f..a95f2f2e759 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -1,4 +1,5 @@ """ core implementation of testing process: init, session, runtest loop. """ +import argparse import fnmatch import functools import importlib @@ -30,6 +31,7 @@ from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit +from _pytest.pathlib import Path from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.runner import collect_one_node @@ -177,6 +179,7 @@ def pytest_addoption(parser: Parser) -> None: "--basetemp", dest="basetemp", default=None, + type=validate_basetemp, metavar="dir", help=( "base temporary directory for this test run." @@ -185,6 +188,34 @@ def pytest_addoption(parser: Parser) -> None: ) +def validate_basetemp(path: str) -> str: + # GH 7119 + msg = "basetemp must not be empty, the current working directory or any parent directory of it" + + # empty path + if not path: + raise argparse.ArgumentTypeError(msg) + + def is_ancestor(base: Path, query: Path) -> bool: + """ return True if query is an ancestor of base, else False.""" + if base == query: + return True + for parent in base.parents: + if parent == query: + return True + return False + + # check if path is an ancestor of cwd + if is_ancestor(Path.cwd(), Path(path).absolute()): + raise argparse.ArgumentTypeError(msg) + + # check symlinks for ancestors + if is_ancestor(Path.cwd().resolve(), Path(path).resolve()): + raise argparse.ArgumentTypeError(msg) + + return path + + def wrap_session( config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]] ) -> Union[int, ExitCode]: diff --git a/testing/test_main.py b/testing/test_main.py index 07aca3a1e24..ee8349a9f33 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -1,7 +1,9 @@ +import argparse from typing import Optional import pytest from _pytest.config import ExitCode +from _pytest.main import validate_basetemp from _pytest.pytester import Testdir @@ -75,3 +77,24 @@ def pytest_sessionfinish(): assert result.ret == ExitCode.NO_TESTS_COLLECTED assert result.stdout.lines[-1] == "collected 0 items" assert result.stderr.lines == ["Exit: exit_pytest_sessionfinish"] + + +@pytest.mark.parametrize("basetemp", ["foo", "foo/bar"]) +def test_validate_basetemp_ok(tmp_path, basetemp, monkeypatch): + monkeypatch.chdir(str(tmp_path)) + validate_basetemp(tmp_path / basetemp) + + +@pytest.mark.parametrize("basetemp", ["", ".", ".."]) +def test_validate_basetemp_fails(tmp_path, basetemp, monkeypatch): + monkeypatch.chdir(str(tmp_path)) + msg = "basetemp must not be empty, the current working directory or any parent directory of it" + with pytest.raises(argparse.ArgumentTypeError, match=msg): + if basetemp: + basetemp = tmp_path / basetemp + validate_basetemp(basetemp) + + +def test_validate_basetemp_integration(testdir): + result = testdir.runpytest("--basetemp=.") + result.stderr.fnmatch_lines("*basetemp must not be*") From fcbaab8b0b89abc622dbfb7982cf9bd8c91ef301 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 8 Jun 2020 22:05:46 -0300 Subject: [PATCH 339/823] Allow tests to override "global" `log_level` (rebased) (#7340) Co-authored-by: Ruaridh Williamson --- AUTHORS | 1 + changelog/7133.improvement.rst | 1 + doc/en/logging.rst | 3 + src/_pytest/logging.py | 10 +++- testing/logging/test_fixture.py | 97 +++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 changelog/7133.improvement.rst diff --git a/AUTHORS b/AUTHORS index 4c5ca41af4b..fdcd5b6e047 100644 --- a/AUTHORS +++ b/AUTHORS @@ -245,6 +245,7 @@ Romain Dorgueil Roman Bolshakov Ronny Pfannschmidt Ross Lawley +Ruaridh Williamson Russel Winder Ryan Wooden Samuel Dion-Girardeau diff --git a/changelog/7133.improvement.rst b/changelog/7133.improvement.rst new file mode 100644 index 00000000000..b537d3e5d6c --- /dev/null +++ b/changelog/7133.improvement.rst @@ -0,0 +1 @@ +``caplog.set_level()`` will now override any :confval:`log_level` set via the CLI or ``.ini``. diff --git a/doc/en/logging.rst b/doc/en/logging.rst index e6f91cdf781..52713854efb 100644 --- a/doc/en/logging.rst +++ b/doc/en/logging.rst @@ -250,6 +250,9 @@ made in ``3.4`` after community feedback: * Log levels are no longer changed unless explicitly requested by the :confval:`log_level` configuration or ``--log-level`` command-line options. This allows users to configure logger objects themselves. + Setting :confval:`log_level` will set the level that is captured globally so if a specific test requires + a lower level than this, use the ``caplog.set_level()`` functionality otherwise that test will be prone to + failure. * :ref:`Live Logs ` is now disabled by default and can be enabled setting the :confval:`log_cli` configuration option to ``true``. When enabled, the verbosity is increased so logging for each test is visible. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index c1f13b701da..ef90c94e862 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -343,7 +343,7 @@ def __init__(self, item: nodes.Node) -> None: """Creates a new funcarg.""" self._item = item # dict of log name -> log level - self._initial_log_levels = {} # type: Dict[Optional[str], int] + self._initial_logger_levels = {} # type: Dict[Optional[str], int] def _finalize(self) -> None: """Finalizes the fixture. @@ -351,7 +351,7 @@ def _finalize(self) -> None: This restores the log levels changed by :meth:`set_level`. """ # restore log levels - for logger_name, level in self._initial_log_levels.items(): + for logger_name, level in self._initial_logger_levels.items(): logger = logging.getLogger(logger_name) logger.setLevel(level) @@ -430,8 +430,9 @@ def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> Non """ logger_obj = logging.getLogger(logger) # save the original log-level to restore it during teardown - self._initial_log_levels.setdefault(logger, logger_obj.level) + self._initial_logger_levels.setdefault(logger, logger_obj.level) logger_obj.setLevel(level) + self.handler.setLevel(level) @contextmanager def at_level( @@ -446,10 +447,13 @@ def at_level( logger_obj = logging.getLogger(logger) orig_level = logger_obj.level logger_obj.setLevel(level) + handler_orig_level = self.handler.level + self.handler.setLevel(level) try: yield finally: logger_obj.setLevel(orig_level) + self.handler.setLevel(handler_orig_level) @pytest.fixture diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index 657ffb4ddf2..3a3663464c2 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -138,3 +138,100 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow # This reaches into private API, don't use this type of thing in real tests! assert set(caplog._item._store[catch_log_records_key]) == {"setup", "call"} + + +def test_ini_controls_global_log_level(testdir): + testdir.makepyfile( + """ + import pytest + import logging + def test_log_level_override(request, caplog): + plugin = request.config.pluginmanager.getplugin('logging-plugin') + assert plugin.log_level == logging.ERROR + logger = logging.getLogger('catchlog') + logger.warning("WARNING message won't be shown") + logger.error("ERROR message will be shown") + assert 'WARNING' not in caplog.text + assert 'ERROR' in caplog.text + """ + ) + testdir.makeini( + """ + [pytest] + log_level=ERROR + """ + ) + + result = testdir.runpytest() + # make sure that that we get a '0' exit code for the testsuite + assert result.ret == 0 + + +def test_caplog_can_override_global_log_level(testdir): + testdir.makepyfile( + """ + import pytest + import logging + def test_log_level_override(request, caplog): + logger = logging.getLogger('catchlog') + plugin = request.config.pluginmanager.getplugin('logging-plugin') + assert plugin.log_level == logging.WARNING + + logger.info("INFO message won't be shown") + + caplog.set_level(logging.INFO, logger.name) + + with caplog.at_level(logging.DEBUG, logger.name): + logger.debug("DEBUG message will be shown") + + logger.debug("DEBUG message won't be shown") + + with caplog.at_level(logging.CRITICAL, logger.name): + logger.warning("WARNING message won't be shown") + + logger.debug("DEBUG message won't be shown") + logger.info("INFO message will be shown") + + assert "message won't be shown" not in caplog.text + """ + ) + testdir.makeini( + """ + [pytest] + log_level=WARNING + """ + ) + + result = testdir.runpytest() + assert result.ret == 0 + + +def test_caplog_captures_despite_exception(testdir): + testdir.makepyfile( + """ + import pytest + import logging + def test_log_level_override(request, caplog): + logger = logging.getLogger('catchlog') + plugin = request.config.pluginmanager.getplugin('logging-plugin') + assert plugin.log_level == logging.WARNING + + logger.info("INFO message won't be shown") + + caplog.set_level(logging.INFO, logger.name) + + with caplog.at_level(logging.DEBUG, logger.name): + logger.debug("DEBUG message will be shown") + raise Exception() + """ + ) + testdir.makeini( + """ + [pytest] + log_level=WARNING + """ + ) + + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*DEBUG message will be shown*"]) + assert result.ret == 1 From 0b70300ba4c00f2fdab1415b33ac6b035418e648 Mon Sep 17 00:00:00 2001 From: piotrhm Date: Mon, 25 May 2020 14:18:57 +0200 Subject: [PATCH 340/823] Added requested modifications --- AUTHORS | 1 + changelog/6471.feature.rst | 1 + src/_pytest/terminal.py | 63 ++++++++++++++++++++++++++------------ 3 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 changelog/6471.feature.rst diff --git a/AUTHORS b/AUTHORS index fdcd5b6e047..821a7d8f41e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -227,6 +227,7 @@ Pedro Algarvio Philipp Loose Pieter Mulder Piotr Banaszkiewicz +Piotr Helm Prashant Anand Pulkit Goyal Punyashloka Biswal diff --git a/changelog/6471.feature.rst b/changelog/6471.feature.rst new file mode 100644 index 00000000000..bc2d7564ed7 --- /dev/null +++ b/changelog/6471.feature.rst @@ -0,0 +1 @@ +New flags --no-header and --no-summary added. The first one disables header, the second one disables summary. \ No newline at end of file diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 9c2665fb818..2a98e4ceb7b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -115,6 +115,20 @@ def pytest_addoption(parser: Parser) -> None: dest="verbose", help="increase verbosity.", ) + group._addoption( + "--no-header", + action="count", + default=0, + dest="no_header", + help="disable header", + ) + group._addoption( + "--no-summary", + action="count", + default=0, + dest="no_summary", + help="disable summary", + ) group._addoption( "-q", "--quiet", @@ -351,6 +365,14 @@ def verbosity(self) -> int: def showheader(self) -> bool: return self.verbosity >= 0 + @property + def no_header(self) -> bool: + return self.config.option.no_header + + @property + def no_summary(self) -> bool: + return self.config.option.no_summary + @property def showfspath(self) -> bool: if self._showfspath is None: @@ -660,25 +682,26 @@ def pytest_sessionstart(self, session: "Session") -> None: return self.write_sep("=", "test session starts", bold=True) verinfo = platform.python_version() - msg = "platform {} -- Python {}".format(sys.platform, verinfo) - pypy_version_info = getattr(sys, "pypy_version_info", None) - if pypy_version_info: - verinfo = ".".join(map(str, pypy_version_info[:3])) - msg += "[pypy-{}-{}]".format(verinfo, pypy_version_info[3]) - msg += ", pytest-{}, py-{}, pluggy-{}".format( - pytest.__version__, py.__version__, pluggy.__version__ - ) - if ( - self.verbosity > 0 - or self.config.option.debug - or getattr(self.config.option, "pastebin", None) - ): - msg += " -- " + str(sys.executable) - self.write_line(msg) - lines = self.config.hook.pytest_report_header( - config=self.config, startdir=self.startdir - ) - self._write_report_lines_from_hooks(lines) + if self.no_header == 0: + msg = "platform {} -- Python {}".format(sys.platform, verinfo) + pypy_version_info = getattr(sys, "pypy_version_info", None) + if pypy_version_info: + verinfo = ".".join(map(str, pypy_version_info[:3])) + msg += "[pypy-{}-{}]".format(verinfo, pypy_version_info[3]) + msg += ", pytest-{}, py-{}, pluggy-{}".format( + pytest.__version__, py.__version__, pluggy.__version__ + ) + if ( + self.verbosity > 0 + or self.config.option.debug + or getattr(self.config.option, "pastebin", None) + ): + msg += " -- " + str(sys.executable) + self.write_line(msg) + lines = self.config.hook.pytest_report_header( + config=self.config, startdir=self.startdir + ) + self._write_report_lines_from_hooks(lines) def _write_report_lines_from_hooks( self, lines: List[Union[str, List[str]]] @@ -775,7 +798,7 @@ def pytest_sessionfinish( ExitCode.USAGE_ERROR, ExitCode.NO_TESTS_COLLECTED, ) - if exitstatus in summary_exit_codes: + if exitstatus in summary_exit_codes and self.no_summary == 0: self.config.hook.pytest_terminal_summary( terminalreporter=self, exitstatus=exitstatus, config=self.config ) From 51fb11c1d1436fb438cfe4d014b34c46fc342b70 Mon Sep 17 00:00:00 2001 From: piotrhm Date: Mon, 25 May 2020 19:29:18 +0200 Subject: [PATCH 341/823] Added tests --- changelog/6471.feature.rst | 2 +- src/_pytest/terminal.py | 12 +++---- testing/test_terminal.py | 74 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/changelog/6471.feature.rst b/changelog/6471.feature.rst index bc2d7564ed7..ebc9a208d1b 100644 --- a/changelog/6471.feature.rst +++ b/changelog/6471.feature.rst @@ -1 +1 @@ -New flags --no-header and --no-summary added. The first one disables header, the second one disables summary. \ No newline at end of file +New flags --no-header and --no-summary added. The first one disables header, the second one disables summary. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2a98e4ceb7b..6a4a609b40b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -117,15 +117,15 @@ def pytest_addoption(parser: Parser) -> None: ) group._addoption( "--no-header", - action="count", - default=0, + action="store_true", + default=False, dest="no_header", help="disable header", ) group._addoption( "--no-summary", - action="count", - default=0, + action="store_true", + default=False, dest="no_summary", help="disable summary", ) @@ -682,7 +682,7 @@ def pytest_sessionstart(self, session: "Session") -> None: return self.write_sep("=", "test session starts", bold=True) verinfo = platform.python_version() - if self.no_header == 0: + if not self.no_header: msg = "platform {} -- Python {}".format(sys.platform, verinfo) pypy_version_info = getattr(sys, "pypy_version_info", None) if pypy_version_info: @@ -798,7 +798,7 @@ def pytest_sessionfinish( ExitCode.USAGE_ERROR, ExitCode.NO_TESTS_COLLECTED, ) - if exitstatus in summary_exit_codes and self.no_summary == 0: + if exitstatus in summary_exit_codes and not self.no_summary: self.config.hook.pytest_terminal_summary( terminalreporter=self, exitstatus=exitstatus, config=self.config ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index e8402079b6c..8b8c246baf4 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -698,6 +698,29 @@ def test_passes(): if request.config.pluginmanager.list_plugin_distinfo(): result.stdout.fnmatch_lines(["plugins: *"]) + def test_no_header_trailer_info(self, testdir, request): + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + testdir.makepyfile( + """ + def test_passes(): + pass + """ + ) + result = testdir.runpytest("--no-header") + verinfo = ".".join(map(str, sys.version_info[:3])) + result.stdout.no_fnmatch_line( + "platform %s -- Python %s*pytest-%s*py-%s*pluggy-%s" + % ( + sys.platform, + verinfo, + pytest.__version__, + py.__version__, + pluggy.__version__, + ) + ) + if request.config.pluginmanager.list_plugin_distinfo(): + result.stdout.no_fnmatch_line("plugins: *") + def test_header(self, testdir): testdir.tmpdir.join("tests").ensure_dir() testdir.tmpdir.join("gui").ensure_dir() @@ -727,6 +750,36 @@ def test_header(self, testdir): result = testdir.runpytest("tests") result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"]) + def test_no_header(self, testdir): + testdir.tmpdir.join("tests").ensure_dir() + testdir.tmpdir.join("gui").ensure_dir() + + # with testpaths option, and not passing anything in the command-line + testdir.makeini( + """ + [pytest] + testpaths = tests gui + """ + ) + result = testdir.runpytest("--no-header") + result.stdout.no_fnmatch_line( + "rootdir: *test_header0, inifile: tox.ini, testpaths: tests, gui" + ) + + # with testpaths option, passing directory in command-line: do not show testpaths then + result = testdir.runpytest("tests", "--no-header") + result.stdout.no_fnmatch_line("rootdir: *test_header0, inifile: tox.ini") + + def test_no_summary(self, testdir): + p1 = testdir.makepyfile( + """ + def test_no_summary(): + assert false + """ + ) + result = testdir.runpytest("--no-summary") + result.stdout.no_fnmatch_line("*= FAILURES =*") + def test_showlocals(self, testdir): p1 = testdir.makepyfile( """ @@ -1483,6 +1536,21 @@ def test_failure(): assert stdout.count("=== warnings summary ") == 1 +@pytest.mark.filterwarnings("default") +def test_terminal_no_summary_warnings_header_once(testdir): + testdir.makepyfile( + """ + def test_failure(): + import warnings + warnings.warn("warning_from_" + "test") + assert 0 + """ + ) + result = testdir.runpytest("--no-summary") + result.stdout.no_fnmatch_line("*= warnings summary =*") + result.stdout.no_fnmatch_line("*= short test summary info =*") + + @pytest.fixture(scope="session") def tr() -> TerminalReporter: config = _pytest.config._prepareconfig() @@ -2130,6 +2198,12 @@ def test_collecterror(testdir): ) +def test_no_summary_collecterror(testdir): + p1 = testdir.makepyfile("raise SyntaxError()") + result = testdir.runpytest("-ra", "--no-summary", str(p1)) + result.stdout.no_fnmatch_line("*= ERRORS =*") + + def test_via_exec(testdir: Testdir) -> None: p1 = testdir.makepyfile("exec('def test_via_exec(): pass')") result = testdir.runpytest(str(p1), "-vv") From 5e0e12d69b2494135e35ef3dcba9434daa932914 Mon Sep 17 00:00:00 2001 From: piotrhm Date: Mon, 25 May 2020 19:36:57 +0200 Subject: [PATCH 342/823] Fixed linting --- testing/test_terminal.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 8b8c246baf4..5b6c7109a1b 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -709,14 +709,14 @@ def test_passes(): result = testdir.runpytest("--no-header") verinfo = ".".join(map(str, sys.version_info[:3])) result.stdout.no_fnmatch_line( - "platform %s -- Python %s*pytest-%s*py-%s*pluggy-%s" - % ( - sys.platform, - verinfo, - pytest.__version__, - py.__version__, - pluggy.__version__, - ) + "platform %s -- Python %s*pytest-%s*py-%s*pluggy-%s" + % ( + sys.platform, + verinfo, + pytest.__version__, + py.__version__, + pluggy.__version__, + ) ) if request.config.pluginmanager.list_plugin_distinfo(): result.stdout.no_fnmatch_line("plugins: *") From 2be1c61eb3a0c202df4ca9ee0d764b5bdaad2001 Mon Sep 17 00:00:00 2001 From: piotrhm Date: Mon, 25 May 2020 20:07:10 +0200 Subject: [PATCH 343/823] Fixed linting 2 --- testing/test_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 5b6c7109a1b..47243335be8 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -779,7 +779,7 @@ def test_no_summary(): ) result = testdir.runpytest("--no-summary") result.stdout.no_fnmatch_line("*= FAILURES =*") - + def test_showlocals(self, testdir): p1 = testdir.makepyfile( """ From df562533ffc467dda8da94c1d87f0722851223eb Mon Sep 17 00:00:00 2001 From: piotrhm Date: Wed, 27 May 2020 12:38:21 +0200 Subject: [PATCH 344/823] Fixed test --- testing/test_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 47243335be8..f1481dce5e4 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -777,7 +777,7 @@ def test_no_summary(): assert false """ ) - result = testdir.runpytest("--no-summary") + result = testdir.runpytest(p1, "--no-summary") result.stdout.no_fnmatch_line("*= FAILURES =*") def test_showlocals(self, testdir): From d5a8bf7c6cfed4950b758a5539fb229497b7dca8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 8 Jun 2020 22:13:29 -0300 Subject: [PATCH 345/823] Improve CHANGELOG --- changelog/6471.feature.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog/6471.feature.rst b/changelog/6471.feature.rst index ebc9a208d1b..28457b9f537 100644 --- a/changelog/6471.feature.rst +++ b/changelog/6471.feature.rst @@ -1 +1,4 @@ -New flags --no-header and --no-summary added. The first one disables header, the second one disables summary. +New command-line flags: + +* `--no-header`: disables the initial header, including platform, version, and plugins. +* `--no-summary`: disables the final test summary, including warnings. From 357f9b6e839d6f7021904b28d974933aeb0f219b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 8 Jun 2020 22:23:28 -0300 Subject: [PATCH 346/823] Add type annotations --- src/_pytest/terminal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 6a4a609b40b..4168f31226b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -367,11 +367,11 @@ def showheader(self) -> bool: @property def no_header(self) -> bool: - return self.config.option.no_header + return bool(self.config.option.no_header) @property def no_summary(self) -> bool: - return self.config.option.no_summary + return bool(self.config.option.no_summary) @property def showfspath(self) -> bool: From 96d4e2f571c59a6b22bf1a68c665ff5c08be87ef Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Mon, 8 Jun 2020 20:37:50 -0400 Subject: [PATCH 347/823] Add documentation on closing issues --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++++ CONTRIBUTING.rst | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d189f7869ce..9408fceafe3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,10 @@ Here is a quick checklist that should be present in PRs. - [ ] Include new tests or update existing tests when applicable. - [X] Allow maintainers to push and squash when merging my commits. Please uncheck this if you prefer to squash the commits yourself. +If this change fixes an issue, please: + +- [ ] Add text like ``closes #XYZW`` to the PR description and/or commits (where ``XYZW`` is the issue number). See the [github docs](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for more information. + Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please: - [ ] Create a new changelog file in the `changelog` folder, with a name like `..rst`. See [changelog/README.rst](https://github.com/pytest-dev/pytest/blob/master/changelog/README.rst) for details. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d5bd7814417..5e309a31728 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -418,3 +418,10 @@ When closing a Pull Request, it needs to be acknowledge the time, effort, and in Again we appreciate your time for working on this, and hope you might get back to this at a later time! + +Closing Issues +-------------- + +When a pull request is submitted to fix an issue, add text like ``closes #XYZW`` to the PR description and/or commits (where ``XYZW`` is the issue number). See the `GitHub docs `_ for more information. + +When an issue is due to user error (e.g. misunderstanding of a functionality), please politely explain to the user why the issue raised is really a non-issue and ask them to close the issue if they have no further questions. If the original requestor is unresponsive, the issue will be handled as described in the section `Handling stale issues/PRs`_ above. From c471b382f5f8e060adcaca092ad3b676634e9025 Mon Sep 17 00:00:00 2001 From: Xinbin Huang Date: Mon, 8 Jun 2020 21:01:11 -0700 Subject: [PATCH 348/823] Remove start_doc_server.sh script --- doc/en/start_doc_server.sh | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 doc/en/start_doc_server.sh diff --git a/doc/en/start_doc_server.sh b/doc/en/start_doc_server.sh deleted file mode 100644 index f68677409be..00000000000 --- a/doc/en/start_doc_server.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "${MY_DIR}"/_build/html || exit -python -m http.server 8000 From bde0ebcda91637366d84fa2c526acb33663c9d22 Mon Sep 17 00:00:00 2001 From: piotrhm Date: Fri, 20 Mar 2020 15:38:55 +0100 Subject: [PATCH 349/823] Replace cleanup_numbered_dir with atexit.register --- AUTHORS | 1 + changelog/1120.bugfix.rst | 1 + src/_pytest/pathlib.py | 14 ++++++++------ 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 changelog/1120.bugfix.rst diff --git a/AUTHORS b/AUTHORS index fdcd5b6e047..821a7d8f41e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -227,6 +227,7 @@ Pedro Algarvio Philipp Loose Pieter Mulder Piotr Banaszkiewicz +Piotr Helm Prashant Anand Pulkit Goyal Punyashloka Biswal diff --git a/changelog/1120.bugfix.rst b/changelog/1120.bugfix.rst new file mode 100644 index 00000000000..96d9887d746 --- /dev/null +++ b/changelog/1120.bugfix.rst @@ -0,0 +1 @@ +Fix issue where directories from tmpdir are not removed properly when multiple instances of pytest are running in parallel. \ No newline at end of file diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 69f490a1d48..8c68fe9e5d3 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -333,16 +333,18 @@ def make_numbered_dir_with_cleanup( try: p = make_numbered_dir(root, prefix) lock_path = create_cleanup_lock(p) - register_cleanup_lock_removal(lock_path) + register_cleanup_lock_removal(lock_path) except Exception as exc: e = exc else: consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout - cleanup_numbered_dir( - root=root, - prefix=prefix, - keep=keep, - consider_lock_dead_if_created_before=consider_lock_dead_if_created_before, + # Register a cleanup for program exit + atexit.register( + cleanup_numbered_dir, + root, + prefix, + keep, + consider_lock_dead_if_created_before, ) return p assert e is not None From f0e47c1ed6b89a5111f5f0a48c600ad91de5b767 Mon Sep 17 00:00:00 2001 From: piotrhm Date: Fri, 20 Mar 2020 16:49:00 +0100 Subject: [PATCH 350/823] Fix typo --- src/_pytest/pathlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 8c68fe9e5d3..29d8c4dc9db 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -333,7 +333,7 @@ def make_numbered_dir_with_cleanup( try: p = make_numbered_dir(root, prefix) lock_path = create_cleanup_lock(p) - register_cleanup_lock_removal(lock_path) + register_cleanup_lock_removal(lock_path) except Exception as exc: e = exc else: From e862643b3fb701db040ca000041915984ae64a22 Mon Sep 17 00:00:00 2001 From: piotrhm Date: Sun, 22 Mar 2020 11:28:24 +0100 Subject: [PATCH 351/823] Update 1120.bugfix.rst --- changelog/1120.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/1120.bugfix.rst b/changelog/1120.bugfix.rst index 96d9887d746..95e74fa75b0 100644 --- a/changelog/1120.bugfix.rst +++ b/changelog/1120.bugfix.rst @@ -1 +1 @@ -Fix issue where directories from tmpdir are not removed properly when multiple instances of pytest are running in parallel. \ No newline at end of file +Fix issue where directories from tmpdir are not removed properly when multiple instances of pytest are running in parallel. From e2e7f15b719f480c4d2a3aea028c55f2dc3f0b75 Mon Sep 17 00:00:00 2001 From: ibriquem Date: Tue, 2 Jun 2020 15:38:41 +0200 Subject: [PATCH 352/823] Make dataclasses/attrs comparison recursive, fixes #4675 --- changelog/4675.bugfix.rst | 1 + src/_pytest/assertion/util.py | 48 ++++++----- .../test_compare_recursive_dataclasses.py | 34 ++++++++ testing/test_assertion.py | 80 +++++++++++++++++++ 4 files changed, 142 insertions(+), 21 deletions(-) create mode 100644 changelog/4675.bugfix.rst create mode 100644 testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py diff --git a/changelog/4675.bugfix.rst b/changelog/4675.bugfix.rst new file mode 100644 index 00000000000..9f857622f08 --- /dev/null +++ b/changelog/4675.bugfix.rst @@ -0,0 +1 @@ +Make dataclasses/attrs comparison recursive. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 7d525aa4c42..c2f0431d479 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -148,26 +148,7 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[ explanation = None try: if op == "==": - if istext(left) and istext(right): - explanation = _diff_text(left, right, verbose) - else: - if issequence(left) and issequence(right): - explanation = _compare_eq_sequence(left, right, verbose) - elif isset(left) and isset(right): - explanation = _compare_eq_set(left, right, verbose) - elif isdict(left) and isdict(right): - explanation = _compare_eq_dict(left, right, verbose) - elif type(left) == type(right) and (isdatacls(left) or isattrs(left)): - type_fn = (isdatacls, isattrs) - explanation = _compare_eq_cls(left, right, verbose, type_fn) - elif verbose > 0: - explanation = _compare_eq_verbose(left, right) - if isiterable(left) and isiterable(right): - expl = _compare_eq_iterable(left, right, verbose) - if explanation is not None: - explanation.extend(expl) - else: - explanation = expl + explanation = _compare_eq_any(left, right, verbose) elif op == "not in": if istext(left) and istext(right): explanation = _notin_text(left, right, verbose) @@ -187,6 +168,28 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[ return [summary] + explanation +def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: + explanation = [] # type: List[str] + if istext(left) and istext(right): + explanation = _diff_text(left, right, verbose) + else: + if issequence(left) and issequence(right): + explanation = _compare_eq_sequence(left, right, verbose) + elif isset(left) and isset(right): + explanation = _compare_eq_set(left, right, verbose) + elif isdict(left) and isdict(right): + explanation = _compare_eq_dict(left, right, verbose) + elif type(left) == type(right) and (isdatacls(left) or isattrs(left)): + type_fn = (isdatacls, isattrs) + explanation = _compare_eq_cls(left, right, verbose, type_fn) + elif verbose > 0: + explanation = _compare_eq_verbose(left, right) + if isiterable(left) and isiterable(right): + expl = _compare_eq_iterable(left, right, verbose) + explanation.extend(expl) + return explanation + + def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: """Return the explanation for the diff between text. @@ -439,7 +442,10 @@ def _compare_eq_cls( explanation += ["Differing attributes:"] for field in diff: explanation += [ - ("%s: %r != %r") % (field, getattr(left, field), getattr(right, field)) + ("%s: %r != %r") % (field, getattr(left, field), getattr(right, field)), + "", + "Drill down into differing attribute %s:" % field, + *_compare_eq_any(getattr(left, field), getattr(right, field), verbose), ] return explanation diff --git a/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py new file mode 100644 index 00000000000..98385379ead --- /dev/null +++ b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass +from dataclasses import field + + +@dataclass +class SimpleDataObject: + field_a: int = field() + field_b: int = field() + + +@dataclass +class ComplexDataObject2: + field_a: SimpleDataObject = field() + field_b: SimpleDataObject = field() + + +@dataclass +class ComplexDataObject: + field_a: SimpleDataObject = field() + field_b: ComplexDataObject2 = field() + + +def test_recursive_dataclasses(): + + left = ComplexDataObject( + SimpleDataObject(1, "b"), + ComplexDataObject2(SimpleDataObject(1, "b"), SimpleDataObject(2, "c"),), + ) + right = ComplexDataObject( + SimpleDataObject(1, "b"), + ComplexDataObject2(SimpleDataObject(1, "b"), SimpleDataObject(3, "c"),), + ) + + assert left == right diff --git a/testing/test_assertion.py b/testing/test_assertion.py index f28876edcc7..4b1df89c93f 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -781,6 +781,48 @@ def test_dataclasses(self, testdir): "*Omitting 1 identical items, use -vv to show*", "*Differing attributes:*", "*field_b: 'b' != 'c'*", + "*- c*", + "*+ b*", + ] + ) + + @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") + def test_recursive_dataclasses(self, testdir): + p = testdir.copy_example("dataclasses/test_compare_recursive_dataclasses.py") + result = testdir.runpytest(p) + result.assert_outcomes(failed=1, passed=0) + result.stdout.fnmatch_lines( + [ + "*Omitting 1 identical items, use -vv to show*", + "*Differing attributes:*", + "*field_b: ComplexDataObject2(*SimpleDataObject(field_a=2, field_b='c')) != ComplexDataObject2(*SimpleDataObject(field_a=3, field_b='c'))*", # noqa + "*Drill down into differing attribute field_b:*", + "*Omitting 1 identical items, use -vv to show*", + "*Differing attributes:*", + "*Full output truncated*", + ] + ) + + @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") + def test_recursive_dataclasses_verbose(self, testdir): + p = testdir.copy_example("dataclasses/test_compare_recursive_dataclasses.py") + result = testdir.runpytest(p, "-vv") + result.assert_outcomes(failed=1, passed=0) + result.stdout.fnmatch_lines( + [ + "*Matching attributes:*", + "*['field_a']*", + "*Differing attributes:*", + "*field_b: ComplexDataObject2(*SimpleDataObject(field_a=2, field_b='c')) != ComplexDataObject2(*SimpleDataObject(field_a=3, field_b='c'))*", # noqa + "*Matching attributes:*", + "*['field_a']*", + "*Differing attributes:*", + "*field_b: SimpleDataObject(field_a=2, field_b='c') " + "!= SimpleDataObject(field_a=3, field_b='c')*", + "*Matching attributes:*", + "*['field_b']*", + "*Differing attributes:*", + "*field_a: 2 != 3", ] ) @@ -832,6 +874,44 @@ class SimpleDataObject: for line in lines[1:]: assert "field_a" not in line + def test_attrs_recursive(self) -> None: + @attr.s + class OtherDataObject: + field_c = attr.ib() + field_d = attr.ib() + + @attr.s + class SimpleDataObject: + field_a = attr.ib() + field_b = attr.ib() + + left = SimpleDataObject(OtherDataObject(1, "a"), "b") + right = SimpleDataObject(OtherDataObject(1, "b"), "b") + + lines = callequal(left, right) + assert "Matching attributes" not in lines + for line in lines[1:]: + assert "field_b:" not in line + assert "field_c:" not in line + + def test_attrs_recursive_verbose(self) -> None: + @attr.s + class OtherDataObject: + field_c = attr.ib() + field_d = attr.ib() + + @attr.s + class SimpleDataObject: + field_a = attr.ib() + field_b = attr.ib() + + left = SimpleDataObject(OtherDataObject(1, "a"), "b") + right = SimpleDataObject(OtherDataObject(1, "b"), "b") + + lines = callequal(left, right) + assert "field_d: 'a' != 'b'" in lines + print("\n".join(lines)) + def test_attrs_verbose(self) -> None: @attr.s class SimpleDataObject: From 09988f3ed1aec29a94f3ac662ef11e99fe1ffafb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 3 Jun 2020 16:06:22 +0300 Subject: [PATCH 353/823] Update testing/test_assertion.py --- testing/test_assertion.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 4b1df89c93f..fcfcf430dea 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -817,8 +817,7 @@ def test_recursive_dataclasses_verbose(self, testdir): "*Matching attributes:*", "*['field_a']*", "*Differing attributes:*", - "*field_b: SimpleDataObject(field_a=2, field_b='c') " - "!= SimpleDataObject(field_a=3, field_b='c')*", + "*field_b: SimpleDataObject(field_a=2, field_b='c') != SimpleDataObject(field_a=3, field_b='c')*", # noqa "*Matching attributes:*", "*['field_b']*", "*Differing attributes:*", From 5a78df4bd059d4c6103217ba9146dcf9d08f989c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 9 Jun 2020 14:43:04 -0300 Subject: [PATCH 354/823] Update CHANGELOG --- changelog/4675.bugfix.rst | 1 - changelog/4675.improvement.rst | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 changelog/4675.bugfix.rst create mode 100644 changelog/4675.improvement.rst diff --git a/changelog/4675.bugfix.rst b/changelog/4675.bugfix.rst deleted file mode 100644 index 9f857622f08..00000000000 --- a/changelog/4675.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Make dataclasses/attrs comparison recursive. diff --git a/changelog/4675.improvement.rst b/changelog/4675.improvement.rst new file mode 100644 index 00000000000..d26e24da2b5 --- /dev/null +++ b/changelog/4675.improvement.rst @@ -0,0 +1 @@ +Rich comparision for dataclasses and `attrs`-classes is now recursive. From c229d6f46ffc77c21ee8773cd341d25d4f8291ba Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 9 Jun 2020 14:48:49 -0300 Subject: [PATCH 355/823] Fix mypy checks --- .../dataclasses/test_compare_recursive_dataclasses.py | 2 +- testing/test_assertion.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py index 98385379ead..516e36e5c85 100644 --- a/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py +++ b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py @@ -5,7 +5,7 @@ @dataclass class SimpleDataObject: field_a: int = field() - field_b: int = field() + field_b: str = field() @dataclass diff --git a/testing/test_assertion.py b/testing/test_assertion.py index fcfcf430dea..ae5e75dbfbb 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -888,6 +888,7 @@ class SimpleDataObject: right = SimpleDataObject(OtherDataObject(1, "b"), "b") lines = callequal(left, right) + assert lines is not None assert "Matching attributes" not in lines for line in lines[1:]: assert "field_b:" not in line @@ -908,8 +909,8 @@ class SimpleDataObject: right = SimpleDataObject(OtherDataObject(1, "b"), "b") lines = callequal(left, right) + assert lines is not None assert "field_d: 'a' != 'b'" in lines - print("\n".join(lines)) def test_attrs_verbose(self) -> None: @attr.s From 10cee92955f9fbd5c39ba2b02e7d8d206458c0eb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 9 Jun 2020 14:58:57 -0300 Subject: [PATCH 356/823] Fix typo --- changelog/4675.improvement.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/4675.improvement.rst b/changelog/4675.improvement.rst index d26e24da2b5..c90cd3591ee 100644 --- a/changelog/4675.improvement.rst +++ b/changelog/4675.improvement.rst @@ -1 +1 @@ -Rich comparision for dataclasses and `attrs`-classes is now recursive. +Rich comparison for dataclasses and `attrs`-classes is now recursive. From 95cb7fb676405fe9281252b68bc80f5de747a4de Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Wed, 10 Jun 2020 00:44:22 -0400 Subject: [PATCH 357/823] review feedback --- changelog/7305.feature.rst | 4 +-- doc/en/reference.rst | 7 ++--- src/_pytest/config/__init__.py | 22 ++++++++++---- testing/test_config.py | 54 +++++++++++++--------------------- 4 files changed, 41 insertions(+), 46 deletions(-) diff --git a/changelog/7305.feature.rst b/changelog/7305.feature.rst index b8c0ca693c7..25978a3961b 100644 --- a/changelog/7305.feature.rst +++ b/changelog/7305.feature.rst @@ -1,3 +1 @@ -New `require_plugins` configuration option allows the user to specify a list of plugins required for pytest to run. Warnings are raised if these plugins are not found when running pytest. - -The `--strict-config` flag can be used to treat these warnings as errors. +New `required_plugins` configuration option allows the user to specify a list of plugins required for pytest to run. An error is raised if any required plugins are not found when running pytest. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 6b270796c8e..f58881d024c 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1561,16 +1561,15 @@ passed multiple times. The expected format is ``name=value``. For example:: See :ref:`change naming conventions` for more detailed examples. -.. confval:: require_plugins +.. confval:: required_plugins A space separated list of plugins that must be present for pytest to run. - If any one of the plugins is not found, emit a warning. - If pytest is run with ``--strict-config`` exceptions are raised in place of warnings. + If any one of the plugins is not found, emit a error. .. code-block:: ini [pytest] - require_plugins = html xdist + required_plugins = pytest-html pytest-xdist .. confval:: testpaths diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index d55a5cdd7d5..483bc617f23 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -953,7 +953,7 @@ def _initini(self, args: Sequence[str]) -> None: self._parser.addini("addopts", "extra command line options", "args") self._parser.addini("minversion", "minimally required pytest version") self._parser.addini( - "require_plugins", + "required_plugins", "plugins that must be present for pytest to run", type="args", default=[], @@ -1090,11 +1090,21 @@ def _validate_keys(self) -> None: self._emit_warning_or_fail("Unknown config ini key: {}\n".format(key)) def _validate_plugins(self) -> None: - for plugin in self.getini("require_plugins"): - if not self.pluginmanager.hasplugin(plugin): - self._emit_warning_or_fail( - "Missing required plugin: {}\n".format(plugin) - ) + plugin_info = self.pluginmanager.list_plugin_distinfo() + plugin_dist_names = [ + "{dist.project_name}".format(dist=dist) for _, dist in plugin_info + ] + + required_plugin_list = [] + for plugin in sorted(self.getini("required_plugins")): + if plugin not in plugin_dist_names: + required_plugin_list.append(plugin) + + if required_plugin_list: + fail( + "Missing required plugins: {}".format(", ".join(required_plugin_list)), + pytrace=False, + ) def _emit_warning_or_fail(self, message: str) -> None: if self.known_args_namespace.strict_config: diff --git a/testing/test_config.py b/testing/test_config.py index f88a9a0ce92..ab7f50ee571 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -213,51 +213,36 @@ def test_invalid_ini_keys( testdir.runpytest("--strict-config") @pytest.mark.parametrize( - "ini_file_text, stderr_output, exception_text", + "ini_file_text, exception_text", [ ( """ [pytest] - require_plugins = fakePlugin1 fakePlugin2 + required_plugins = fakePlugin1 fakePlugin2 """, - [ - "WARNING: Missing required plugin: fakePlugin1", - "WARNING: Missing required plugin: fakePlugin2", - ], - "Missing required plugin: fakePlugin1", + "Missing required plugins: fakePlugin1, fakePlugin2", ), ( """ [pytest] - require_plugins = a monkeypatch z + required_plugins = a pytest-xdist z """, - [ - "WARNING: Missing required plugin: a", - "WARNING: Missing required plugin: z", - ], - "Missing required plugin: a", + "Missing required plugins: a, z", ), ( """ [pytest] - require_plugins = a monkeypatch z - addopts = -p no:monkeypatch + required_plugins = a q j b c z """, - [ - "WARNING: Missing required plugin: a", - "WARNING: Missing required plugin: monkeypatch", - "WARNING: Missing required plugin: z", - ], - "Missing required plugin: a", + "Missing required plugins: a, b, c, j, q, z", ), ( """ [some_other_header] - require_plugins = wont be triggered + required_plugins = wont be triggered [pytest] minversion = 5.0.0 """, - [], "", ), ( @@ -265,23 +250,21 @@ def test_invalid_ini_keys( [pytest] minversion = 5.0.0 """, - [], "", ), ], ) - def test_missing_required_plugins( - self, testdir, ini_file_text, stderr_output, exception_text - ): - testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) - testdir.parseconfig() + def test_missing_required_plugins(self, testdir, ini_file_text, exception_text): + pytest.importorskip("xdist") - result = testdir.runpytest() - result.stderr.fnmatch_lines(stderr_output) + testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") - if stderr_output: + if exception_text: with pytest.raises(pytest.fail.Exception, match=exception_text): - testdir.runpytest("--strict-config") + testdir.parseconfig() + else: + testdir.parseconfig() class TestConfigCmdlineParsing: @@ -681,6 +664,7 @@ class PseudoPlugin: class Dist: files = () + metadata = {"name": "foo"} entry_points = (EntryPoint(),) def my_dists(): @@ -711,6 +695,7 @@ def load(self): class Distribution: version = "1.0" files = ("foo.txt",) + metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) def distributions(): @@ -735,6 +720,7 @@ def load(self): class Distribution: version = "1.0" files = None + metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) def distributions(): @@ -760,6 +746,7 @@ def load(self): class Distribution: version = "1.0" files = ("foo.txt",) + metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) def distributions(): @@ -791,6 +778,7 @@ def load(self): return sys.modules[self.name] class Distribution: + metadata = {"name": "foo"} entry_points = (DummyEntryPoint(),) files = () From e36d5c05c69c6ba82d4a5517389493a614137939 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 13:08:27 +0200 Subject: [PATCH 358/823] doc: Explain indirect parametrization and markers for fixtures --- changelog/7345.doc.rst | 1 + doc/en/example/parametrize.rst | 24 ++++++++++++++++++++++++ doc/en/fixture.rst | 31 +++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 changelog/7345.doc.rst diff --git a/changelog/7345.doc.rst b/changelog/7345.doc.rst new file mode 100644 index 00000000000..4c7234f4144 --- /dev/null +++ b/changelog/7345.doc.rst @@ -0,0 +1 @@ +Explain indirect parametrization and markers for fixtures diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 9500af0d343..0b61a19bc91 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -351,6 +351,30 @@ And then when we run the test: The first invocation with ``db == "DB1"`` passed while the second with ``db == "DB2"`` failed. Our ``db`` fixture function has instantiated each of the DB values during the setup phase while the ``pytest_generate_tests`` generated two according calls to the ``test_db_initialized`` during the collection phase. +Indirect parametrization +--------------------------------------------------- + +Using the ``indirect=True`` parameter when parametrizing a test allows to +parametrize a test with a fixture receiving the values before passing them to a +test: + +.. code-block:: python + + import pytest + + + @pytest.fixture + def fixt(request): + return request.param * 3 + + + @pytest.mark.parametrize("fixt", ["a", "b"], indirect=True) + def test_indirect(fixt): + assert len(fixt) == 3 + +This can be used, for example, to do more expensive setup at test run time in +the fixture, rather than having to run those setup steps at collection time. + .. regendoc:wipe Apply indirect on particular arguments diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 925a4b55982..b529996ede8 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -665,6 +665,37 @@ Running it: voila! The ``smtp_connection`` fixture function picked up our mail server name from the module namespace. +.. _`using-markers`: + +Using markers to pass data to fixtures +------------------------------------------------------------- + +Using the :py:class:`request ` object, a fixture can also access +markers which are applied to a test function. This can be useful to pass data +into a fixture from a test: + +.. code-block:: python + + import pytest + + + @pytest.fixture + def fixt(request): + marker = request.node.get_closest_marker("fixt_data") + if marker is None: + # Handle missing marker in some way... + data = None + else: + data = marker.args[0] + + # Do something with the data + return data + + + @pytest.mark.fixt_data(42) + def test_fixt(fixt): + assert fixt == 42 + .. _`fixture-factory`: Factories as fixtures From c18afb59f50d280c34e1a7a1fd4a49831952d860 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Wed, 10 Jun 2020 19:09:24 -0400 Subject: [PATCH 359/823] final touches --- src/_pytest/config/__init__.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 483bc617f23..16bf75b6ee5 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1090,19 +1090,21 @@ def _validate_keys(self) -> None: self._emit_warning_or_fail("Unknown config ini key: {}\n".format(key)) def _validate_plugins(self) -> None: + required_plugins = sorted(self.getini("required_plugins")) + if not required_plugins: + return + plugin_info = self.pluginmanager.list_plugin_distinfo() - plugin_dist_names = [ - "{dist.project_name}".format(dist=dist) for _, dist in plugin_info - ] + plugin_dist_names = [dist.project_name for _, dist in plugin_info] - required_plugin_list = [] - for plugin in sorted(self.getini("required_plugins")): + missing_plugins = [] + for plugin in required_plugins: if plugin not in plugin_dist_names: - required_plugin_list.append(plugin) + missing_plugins.append(plugin) - if required_plugin_list: + if missing_plugins: fail( - "Missing required plugins: {}".format(", ".join(required_plugin_list)), + "Missing required plugins: {}".format(", ".join(missing_plugins)), pytrace=False, ) From 68572179cb6296fd856c75e7395b5a578d12901f Mon Sep 17 00:00:00 2001 From: Martin Michlmayr Date: Thu, 11 Jun 2020 16:22:47 +0800 Subject: [PATCH 360/823] doc: Fix typos and cosmetic issues --- doc/en/assert.rst | 2 +- doc/en/fixture.rst | 6 +++--- doc/en/parametrize.rst | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 5ece98e963d..a3a34a9c653 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -98,7 +98,7 @@ and if you need to have access to the actual exception info you may use: f() assert "maximum recursion" in str(excinfo.value) -``excinfo`` is a ``ExceptionInfo`` instance, which is a wrapper around +``excinfo`` is an ``ExceptionInfo`` instance, which is a wrapper around the actual exception raised. The main attributes of interest are ``.type``, ``.value`` and ``.traceback``. diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index b529996ede8..a4e262c2fa3 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -179,7 +179,7 @@ In the failure traceback we see that the test function was called with a function. The test function fails on our deliberate ``assert 0``. Here is the exact protocol used by ``pytest`` to call the test function this way: -1. pytest :ref:`finds ` the ``test_ehlo`` because +1. pytest :ref:`finds ` the test ``test_ehlo`` because of the ``test_`` prefix. The test function needs a function argument named ``smtp_connection``. A matching fixture function is discovered by looking for a fixture-marked function named ``smtp_connection``. @@ -859,7 +859,7 @@ be used with ``-k`` to select specific cases to run, and they will also identify the specific case when one is failing. Running pytest with ``--collect-only`` will show the generated IDs. -Numbers, strings, booleans and None will have their usual string +Numbers, strings, booleans and ``None`` will have their usual string representation used in the test ID. For other objects, pytest will make a string based on the argument name. It is possible to customise the string used in a test ID for a certain fixture value by using the @@ -898,7 +898,7 @@ the string used in a test ID for a certain fixture value by using the The above shows how ``ids`` can be either a list of strings to use or a function which will be called with the fixture value and then has to return a string to use. In the latter case if the function -return ``None`` then pytest's auto-generated ID will be used. +returns ``None`` then pytest's auto-generated ID will be used. Running the above tests results in the following test IDs being used: diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 29223e28e6b..1e356ebb3ca 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -133,7 +133,7 @@ Let's run this: ======================= 2 passed, 1 xfailed in 0.12s ======================= The one parameter set which caused a failure previously now -shows up as an "xfailed (expected to fail)" test. +shows up as an "xfailed" (expected to fail) test. In case the values provided to ``parametrize`` result in an empty list - for example, if they're dynamically generated by some function - the behaviour of From 57415e68ee6355325410e2326039262f7c605360 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Thu, 11 Jun 2020 16:55:25 -0400 Subject: [PATCH 361/823] Update changelog/7305.feature.rst Co-authored-by: Ran Benita --- changelog/7305.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/7305.feature.rst b/changelog/7305.feature.rst index 25978a3961b..96b7f72eed9 100644 --- a/changelog/7305.feature.rst +++ b/changelog/7305.feature.rst @@ -1 +1 @@ -New `required_plugins` configuration option allows the user to specify a list of plugins required for pytest to run. An error is raised if any required plugins are not found when running pytest. +New ``required_plugins`` configuration option allows the user to specify a list of plugins required for pytest to run. An error is raised if any required plugins are not found when running pytest. From ab331c906e9047383eee11f4929b7edefe82b63e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 11 Jun 2020 16:47:59 -0300 Subject: [PATCH 362/823] Suppress errors while removing tmpdir's lock files Fix #5456 --- changelog/5456.bugfix.rst | 2 ++ src/_pytest/pathlib.py | 13 +++++++++---- testing/test_pathlib.py | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 changelog/5456.bugfix.rst diff --git a/changelog/5456.bugfix.rst b/changelog/5456.bugfix.rst new file mode 100644 index 00000000000..17680757052 --- /dev/null +++ b/changelog/5456.bugfix.rst @@ -0,0 +1,2 @@ +Fix a possible race condition when trying to remove lock files used to control access to folders +created by ``tmp_path`` and ``tmpdir``. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 29d8c4dc9db..98ec936a119 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -1,4 +1,5 @@ import atexit +import contextlib import fnmatch import itertools import os @@ -290,10 +291,14 @@ def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> return False else: if lock_time < consider_lock_dead_if_created_before: - lock.unlink() - return True - else: - return False + # wa want to ignore any errors while trying to remove the lock such as: + # - PermissionDenied, like the file permissions have changed since the lock creation + # - FileNotFoundError, in case another pytest process got here first. + # and any other cause of failure. + with contextlib.suppress(OSError): + lock.unlink() + return True + return False def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None: diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 03bed26ec3a..acc963199bc 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -1,9 +1,11 @@ import os.path import sys +import unittest.mock import py import pytest +from _pytest.pathlib import ensure_deletable from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import get_extended_length_path_str from _pytest.pathlib import get_lock_path @@ -113,3 +115,22 @@ def test_get_extended_length_path_str(): assert get_extended_length_path_str(r"\\share\foo") == r"\\?\UNC\share\foo" assert get_extended_length_path_str(r"\\?\UNC\share\foo") == r"\\?\UNC\share\foo" assert get_extended_length_path_str(r"\\?\c:\foo") == r"\\?\c:\foo" + + +def test_suppress_error_removing_lock(tmp_path): + """ensure_deletable should not raise an exception if the lock file cannot be removed (#5456)""" + path = tmp_path / "dir" + path.mkdir() + lock = get_lock_path(path) + lock.touch() + mtime = lock.stat().st_mtime + + with unittest.mock.patch.object(Path, "unlink", side_effect=OSError): + assert not ensure_deletable( + path, consider_lock_dead_if_created_before=mtime + 30 + ) + assert lock.is_file() + + # check now that we can remove the lock file in normal circumstances + assert ensure_deletable(path, consider_lock_dead_if_created_before=mtime + 30) + assert not lock.is_file() From 2c8e356174d9760a28f5ff6a3b5754417d41b7bc Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Fri, 12 Jun 2020 08:27:55 -0400 Subject: [PATCH 363/823] rename _emit_warning_or_fail to _warn_or_fail_if_strict and fix a doc typo --- doc/en/reference.rst | 2 +- src/_pytest/config/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index f58881d024c..2fab4160c7b 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1564,7 +1564,7 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: required_plugins A space separated list of plugins that must be present for pytest to run. - If any one of the plugins is not found, emit a error. + If any one of the plugins is not found, emit an error. .. code-block:: ini diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 16bf75b6ee5..07985df2dcd 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1087,7 +1087,7 @@ def _checkversion(self): def _validate_keys(self) -> None: for key in sorted(self._get_unknown_ini_keys()): - self._emit_warning_or_fail("Unknown config ini key: {}\n".format(key)) + self._warn_or_fail_if_strict("Unknown config ini key: {}\n".format(key)) def _validate_plugins(self) -> None: required_plugins = sorted(self.getini("required_plugins")) @@ -1108,7 +1108,7 @@ def _validate_plugins(self) -> None: pytrace=False, ) - def _emit_warning_or_fail(self, message: str) -> None: + def _warn_or_fail_if_strict(self, message: str) -> None: if self.known_args_namespace.strict_config: fail(message, pytrace=False) sys.stderr.write("WARNING: {}".format(message)) From 564b2f707dd558d10974268ab5a5494de2f90238 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Fri, 12 Jun 2020 22:13:52 +1000 Subject: [PATCH 364/823] Finish deprecation of "slave" --- changelog/7356.trivial.rst | 1 + doc/en/announce/release-2.3.5.rst | 2 +- doc/en/announce/release-2.6.1.rst | 2 +- doc/en/changelog.rst | 6 +++--- doc/en/funcarg_compare.rst | 2 +- src/_pytest/cacheprovider.py | 4 ++-- src/_pytest/junitxml.py | 12 ++++++------ src/_pytest/pastebin.py | 2 +- src/_pytest/reports.py | 10 +++++----- src/_pytest/resultlog.py | 4 ++-- testing/test_junitxml.py | 8 ++++---- testing/test_resultlog.py | 4 ++-- 12 files changed, 29 insertions(+), 28 deletions(-) create mode 100644 changelog/7356.trivial.rst diff --git a/changelog/7356.trivial.rst b/changelog/7356.trivial.rst new file mode 100644 index 00000000000..d280e229125 --- /dev/null +++ b/changelog/7356.trivial.rst @@ -0,0 +1 @@ +Remove last internal uses of deprecated "slave" term from old pytest-xdist. diff --git a/doc/en/announce/release-2.3.5.rst b/doc/en/announce/release-2.3.5.rst index 465dd826ed4..d68780a2440 100644 --- a/doc/en/announce/release-2.3.5.rst +++ b/doc/en/announce/release-2.3.5.rst @@ -46,7 +46,7 @@ Changes between 2.3.4 and 2.3.5 - Issue 265 - integrate nose setup/teardown with setupstate so it doesn't try to teardown if it did not setup -- issue 271 - don't write junitxml on slave nodes +- issue 271 - don't write junitxml on worker nodes - Issue 274 - don't try to show full doctest example when doctest does not know the example location diff --git a/doc/en/announce/release-2.6.1.rst b/doc/en/announce/release-2.6.1.rst index fba6f2993a5..85d9861643a 100644 --- a/doc/en/announce/release-2.6.1.rst +++ b/doc/en/announce/release-2.6.1.rst @@ -32,7 +32,7 @@ Changes 2.6.1 purely the nodeid. The line number is still shown in failure reports. Thanks Floris Bruynooghe. -- fix issue437 where assertion rewriting could cause pytest-xdist slaves +- fix issue437 where assertion rewriting could cause pytest-xdist worker nodes to collect different tests. Thanks Bruno Oliveira. - fix issue555: add "errors" attribute to capture-streams to satisfy diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 1a298072b0f..2806fb6a3e5 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -6159,7 +6159,7 @@ time or change existing behaviors in order to make them less surprising/more use purely the nodeid. The line number is still shown in failure reports. Thanks Floris Bruynooghe. -- fix issue437 where assertion rewriting could cause pytest-xdist slaves +- fix issue437 where assertion rewriting could cause pytest-xdist worker nodes to collect different tests. Thanks Bruno Oliveira. - fix issue555: add "errors" attribute to capture-streams to satisfy @@ -6706,7 +6706,7 @@ Bug fixes: - Issue 265 - integrate nose setup/teardown with setupstate so it doesn't try to teardown if it did not setup -- issue 271 - don't write junitxml on slave nodes +- issue 271 - don't write junitxml on worker nodes - Issue 274 - don't try to show full doctest example when doctest does not know the example location @@ -7588,7 +7588,7 @@ Bug fixes: - fix assert reinterpreation that sees a call containing "keyword=..." - fix issue66: invoke pytest_sessionstart and pytest_sessionfinish - hooks on slaves during dist-testing, report module/session teardown + hooks on worker nodes during dist-testing, report module/session teardown hooks correctly. - fix issue65: properly handle dist-testing if no diff --git a/doc/en/funcarg_compare.rst b/doc/en/funcarg_compare.rst index af70301654d..4350c98b670 100644 --- a/doc/en/funcarg_compare.rst +++ b/doc/en/funcarg_compare.rst @@ -170,7 +170,7 @@ several problems: 1. in distributed testing the master process would setup test resources that are never needed because it only co-ordinates the test run - activities of the slave processes. + activities of the worker processes. 2. if you only perform a collection (with "--collect-only") resource-setup will still be executed. diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index af7d57a2490..9baee1d4e33 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -341,7 +341,7 @@ def pytest_collection_modifyitems( def pytest_sessionfinish(self, session: Session) -> None: config = self.config - if config.getoption("cacheshow") or hasattr(config, "slaveinput"): + if config.getoption("cacheshow") or hasattr(config, "workerinput"): return assert config.cache is not None @@ -386,7 +386,7 @@ def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item] def pytest_sessionfinish(self) -> None: config = self.config - if config.getoption("cacheshow") or hasattr(config, "slaveinput"): + if config.getoption("cacheshow") or hasattr(config, "workerinput"): return if config.getoption("collectonly"): diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 47ba89d38cd..e62bc5235e6 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -427,8 +427,8 @@ def pytest_addoption(parser: Parser) -> None: def pytest_configure(config: Config) -> None: xmlpath = config.option.xmlpath - # prevent opening xmllog on slave nodes (xdist) - if xmlpath and not hasattr(config, "slaveinput"): + # prevent opening xmllog on worker nodes (xdist) + if xmlpath and not hasattr(config, "workerinput"): junit_family = config.getini("junit_family") if not junit_family: _issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2) @@ -506,17 +506,17 @@ def __init__( def finalize(self, report: TestReport) -> None: nodeid = getattr(report, "nodeid", report) # local hack to handle xdist report order - slavenode = getattr(report, "node", None) - reporter = self.node_reporters.pop((nodeid, slavenode)) + workernode = getattr(report, "node", None) + reporter = self.node_reporters.pop((nodeid, workernode)) if reporter is not None: reporter.finalize() def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter: nodeid = getattr(report, "nodeid", report) # type: Union[str, TestReport] # local hack to handle xdist report order - slavenode = getattr(report, "node", None) + workernode = getattr(report, "node", None) - key = nodeid, slavenode + key = nodeid, workernode if key in self.node_reporters: # TODO: breaks for --dist=each diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 7e6bbf50cbe..a3432c7a10c 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -33,7 +33,7 @@ def pytest_configure(config: Config) -> None: if config.option.pastebin == "all": tr = config.pluginmanager.getplugin("terminalreporter") # if no terminal reporter plugin is present, nothing we can do here; - # this can happen when this function executes in a slave node + # this can happen when this function executes in a worker node # when using pytest-xdist, for example if tr is not None: # pastebin file will be utf-8 encoded binary file diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 7462cea0b69..6a408354b03 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -38,13 +38,13 @@ from _pytest.runner import CallInfo -def getslaveinfoline(node): +def getworkerinfoline(node): try: - return node._slaveinfocache + return node._workerinfocache except AttributeError: - d = node.slaveinfo + d = node.workerinfo ver = "%s.%s.%s" % d["version_info"][:3] - node._slaveinfocache = s = "[{}] {} -- Python {} {}".format( + node._workerinfocache = s = "[{}] {} -- Python {} {}".format( d["id"], d["sysplatform"], ver, d["executable"] ) return s @@ -71,7 +71,7 @@ def __getattr__(self, key: str) -> Any: def toterminal(self, out) -> None: if hasattr(self, "node"): - out.line(getslaveinfoline(self.node)) + out.line(getworkerinfoline(self.node)) longrepr = self.longrepr if longrepr is None: diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index c2b0cf5563a..c870ef08eae 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -29,8 +29,8 @@ def pytest_addoption(parser: Parser) -> None: def pytest_configure(config: Config) -> None: resultlog = config.option.resultlog - # prevent opening resultlog on slave nodes (xdist) - if resultlog and not hasattr(config, "slaveinput"): + # prevent opening resultlog on worker nodes (xdist) + if resultlog and not hasattr(config, "workerinput"): dirname = os.path.dirname(os.path.abspath(resultlog)) if not os.path.isdir(dirname): os.makedirs(dirname) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index d7771cc9708..f8a6a295f6b 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -866,12 +866,12 @@ def test_mangle_test_address(): assert newnames == ["a.my.py.thing", "Class", "method", "[a-1-::]"] -def test_dont_configure_on_slaves(tmpdir) -> None: +def test_dont_configure_on_workers(tmpdir) -> None: gotten = [] # type: List[object] class FakeConfig: if TYPE_CHECKING: - slaveinput = None + workerinput = None def __init__(self): self.pluginmanager = self @@ -891,7 +891,7 @@ def getini(self, name): junitxml.pytest_configure(fake_config) assert len(gotten) == 1 - FakeConfig.slaveinput = None + FakeConfig.workerinput = None junitxml.pytest_configure(fake_config) assert len(gotten) == 1 @@ -1250,7 +1250,7 @@ def test_record({fixture_name}, other): def test_random_report_log_xdist(testdir, monkeypatch, run_and_parse): - """xdist calls pytest_runtest_logreport as they are executed by the slaves, + """xdist calls pytest_runtest_logreport as they are executed by the workers, with nodes from several nodes overlapping, so junitxml must cope with that to produce correct reports. #1064 """ diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index bad575e3d13..8fc93d25c7d 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -177,7 +177,7 @@ def test_pass(): LineMatcher(lines).fnmatch_lines([". *:test_pass"]) -def test_no_resultlog_on_slaves(testdir): +def test_no_resultlog_on_workers(testdir): config = testdir.parseconfig("-p", "resultlog", "--resultlog=resultlog") assert resultlog_key not in config._store @@ -186,7 +186,7 @@ def test_no_resultlog_on_slaves(testdir): pytest_unconfigure(config) assert resultlog_key not in config._store - config.slaveinput = {} + config.workerinput = {} pytest_configure(config) assert resultlog_key not in config._store pytest_unconfigure(config) From f84ffd974719dd2500968fe44e5d63c9562812e8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 8 Jun 2020 21:21:58 +0300 Subject: [PATCH 365/823] Remove unused type: ignores Not needed since update from mypy 0.770 -> 0.780. --- src/_pytest/cacheprovider.py | 3 +-- src/_pytest/capture.py | 8 +++----- src/_pytest/config/__init__.py | 4 +--- src/_pytest/mark/structures.py | 4 +--- testing/test_assertrewrite.py | 2 +- 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 9baee1d4e33..305a122e9ae 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -464,8 +464,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: @pytest.hookimpl(tryfirst=True) def pytest_configure(config: Config) -> None: - # Type ignored: pending mechanism to store typed objects scoped to config. - config.cache = Cache.for_config(config) # type: ignore # noqa: F821 + config.cache = Cache.for_config(config) config.pluginmanager.register(LFPlugin(config), "lfplugin") config.pluginmanager.register(NFPlugin(config), "nfplugin") diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 04104128456..6009e1f67b7 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -519,11 +519,10 @@ def start_capturing(self) -> None: def pop_outerr_to_orig(self): """ pop current snapshot out/err capture and flush to orig streams. """ out, err = self.readouterr() - # TODO: Fix type ignores. if out: - self.out.writeorg(out) # type: ignore[union-attr] # noqa: F821 + self.out.writeorg(out) if err: - self.err.writeorg(err) # type: ignore[union-attr] # noqa: F821 + self.err.writeorg(err) return out, err def suspend_capturing(self, in_: bool = False) -> None: @@ -543,8 +542,7 @@ def resume_capturing(self) -> None: if self.err: self.err.resume() if self._in_suspended: - # TODO: Fix type ignore. - self.in_.resume() # type: ignore[union-attr] # noqa: F821 + self.in_.resume() self._in_suspended = False def stop_capturing(self) -> None: diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c94ea2a9319..daccdc6a1d1 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -988,9 +988,7 @@ def _mark_plugins_for_rewrite(self, hook) -> None: package_files = ( str(file) for dist in importlib_metadata.distributions() - # Type ignored due to missing stub: - # https://github.com/python/typeshed/pull/3795 - if any(ep.group == "pytest11" for ep in dist.entry_points) # type: ignore + if any(ep.group == "pytest11" for ep in dist.entry_points) for file in dist.files or [] ) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 7abff9b7b83..3d512816c7c 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -66,9 +66,7 @@ def get_empty_parameterset_mark( fs, lineno, ) - # Type ignored because MarkDecorator.__call__() is a bit tough to - # annotate ATM. - return mark(reason=reason) # type: ignore[no-any-return] # noqa: F723 + return mark(reason=reason) class ParameterSet( diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 3813993bec1..38893deb9a2 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1258,7 +1258,7 @@ def isinitpath(self, p): def spy_find_spec(name, path): self.find_spec_calls.append(name) - return importlib.machinery.PathFinder.find_spec(name, path) # type: ignore + return importlib.machinery.PathFinder.find_spec(name, path) hook = AssertionRewritingHook(pytestconfig) # use default patterns, otherwise we inherit pytest's testing config From b4f046b7777c7f7f45fbb18ac02100cd5459a02e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 7 Jun 2020 12:44:11 +0300 Subject: [PATCH 366/823] monkeypatch: add type annotations --- src/_pytest/monkeypatch.py | 95 +++++++++++++++++++++++++++---------- testing/test_monkeypatch.py | 80 ++++++++++++++++--------------- 2 files changed, 113 insertions(+), 62 deletions(-) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 9d802a62578..09f1ac36e52 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -4,17 +4,29 @@ import sys import warnings from contextlib import contextmanager +from typing import Any from typing import Generator +from typing import List +from typing import MutableMapping +from typing import Optional +from typing import Tuple +from typing import TypeVar +from typing import Union import pytest +from _pytest.compat import overload from _pytest.fixtures import fixture from _pytest.pathlib import Path RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") +K = TypeVar("K") +V = TypeVar("V") + + @fixture -def monkeypatch(): +def monkeypatch() -> Generator["MonkeyPatch", None, None]: """The returned ``monkeypatch`` fixture provides these helper methods to modify objects, dictionaries or os.environ:: @@ -37,7 +49,7 @@ def monkeypatch(): mpatch.undo() -def resolve(name): +def resolve(name: str) -> object: # simplified from zope.dottedname parts = name.split(".") @@ -66,7 +78,7 @@ def resolve(name): return found -def annotated_getattr(obj, name, ann): +def annotated_getattr(obj: object, name: str, ann: str) -> object: try: obj = getattr(obj, name) except AttributeError: @@ -78,7 +90,7 @@ def annotated_getattr(obj, name, ann): return obj -def derive_importpath(import_path, raising): +def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]: if not isinstance(import_path, str) or "." not in import_path: raise TypeError( "must be absolute import path string, not {!r}".format(import_path) @@ -91,7 +103,7 @@ def derive_importpath(import_path, raising): class Notset: - def __repr__(self): + def __repr__(self) -> str: return "" @@ -102,11 +114,13 @@ class MonkeyPatch: """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes. """ - def __init__(self): - self._setattr = [] - self._setitem = [] - self._cwd = None - self._savesyspath = None + def __init__(self) -> None: + self._setattr = [] # type: List[Tuple[object, str, object]] + self._setitem = ( + [] + ) # type: List[Tuple[MutableMapping[Any, Any], object, object]] + self._cwd = None # type: Optional[str] + self._savesyspath = None # type: Optional[List[str]] @contextmanager def context(self) -> Generator["MonkeyPatch", None, None]: @@ -133,7 +147,25 @@ def test_partial(monkeypatch): finally: m.undo() - def setattr(self, target, name, value=notset, raising=True): + @overload + def setattr( + self, target: str, name: object, value: Notset = ..., raising: bool = ..., + ) -> None: + raise NotImplementedError() + + @overload # noqa: F811 + def setattr( # noqa: F811 + self, target: object, name: str, value: object, raising: bool = ..., + ) -> None: + raise NotImplementedError() + + def setattr( # noqa: F811 + self, + target: Union[str, object], + name: Union[object, str], + value: object = notset, + raising: bool = True, + ) -> None: """ Set attribute value on target, memorizing the old value. By default raise AttributeError if the attribute did not exist. @@ -150,7 +182,7 @@ def setattr(self, target, name, value=notset, raising=True): __tracebackhide__ = True import inspect - if value is notset: + if isinstance(value, Notset): if not isinstance(target, str): raise TypeError( "use setattr(target, name, value) or " @@ -159,6 +191,13 @@ def setattr(self, target, name, value=notset, raising=True): ) value = name name, target = derive_importpath(target, raising) + else: + if not isinstance(name, str): + raise TypeError( + "use setattr(target, name, value) with name being a string or " + "setattr(target, value) with target being a dotted " + "import string" + ) oldval = getattr(target, name, notset) if raising and oldval is notset: @@ -170,7 +209,12 @@ def setattr(self, target, name, value=notset, raising=True): self._setattr.append((target, name, oldval)) setattr(target, name, value) - def delattr(self, target, name=notset, raising=True): + def delattr( + self, + target: Union[object, str], + name: Union[str, Notset] = notset, + raising: bool = True, + ) -> None: """ Delete attribute ``name`` from ``target``, by default raise AttributeError it the attribute did not previously exist. @@ -184,7 +228,7 @@ def delattr(self, target, name=notset, raising=True): __tracebackhide__ = True import inspect - if name is notset: + if isinstance(name, Notset): if not isinstance(target, str): raise TypeError( "use delattr(target, name) or " @@ -204,12 +248,12 @@ def delattr(self, target, name=notset, raising=True): self._setattr.append((target, name, oldval)) delattr(target, name) - def setitem(self, dic, name, value): + def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None: """ Set dictionary entry ``name`` to value. """ self._setitem.append((dic, name, dic.get(name, notset))) dic[name] = value - def delitem(self, dic, name, raising=True): + def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None: """ Delete ``name`` from dict. Raise KeyError if it doesn't exist. If ``raising`` is set to False, no exception will be raised if the @@ -222,7 +266,7 @@ def delitem(self, dic, name, raising=True): self._setitem.append((dic, name, dic.get(name, notset))) del dic[name] - def setenv(self, name, value, prepend=None): + def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: """ Set environment variable ``name`` to ``value``. If ``prepend`` is a character, read the current environment variable value and prepend the ``value`` adjoined with the ``prepend`` character.""" @@ -241,16 +285,17 @@ def setenv(self, name, value, prepend=None): value = value + prepend + os.environ[name] self.setitem(os.environ, name, value) - def delenv(self, name, raising=True): + def delenv(self, name: str, raising: bool = True) -> None: """ Delete ``name`` from the environment. Raise KeyError if it does not exist. If ``raising`` is set to False, no exception will be raised if the environment variable is missing. """ - self.delitem(os.environ, name, raising=raising) + environ = os.environ # type: MutableMapping[str, str] + self.delitem(environ, name, raising=raising) - def syspath_prepend(self, path): + def syspath_prepend(self, path) -> None: """ Prepend ``path`` to ``sys.path`` list of import locations. """ from pkg_resources import fixup_namespace_packages @@ -272,7 +317,7 @@ def syspath_prepend(self, path): invalidate_caches() - def chdir(self, path): + def chdir(self, path) -> None: """ Change the current working directory to the specified path. Path can be a string or a py.path.local object. """ @@ -286,7 +331,7 @@ def chdir(self, path): else: os.chdir(path) - def undo(self): + def undo(self) -> None: """ Undo previous changes. This call consumes the undo stack. Calling it a second time has no effect unless you do more monkeypatching after the undo call. @@ -306,14 +351,14 @@ def undo(self): else: delattr(obj, name) self._setattr[:] = [] - for dictionary, name, value in reversed(self._setitem): + for dictionary, key, value in reversed(self._setitem): if value is notset: try: - del dictionary[name] + del dictionary[key] except KeyError: pass # was already deleted, so we have the desired state else: - dictionary[name] = value + dictionary[key] = value self._setitem[:] = [] if self._savesyspath is not None: sys.path[:] = self._savesyspath diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 1a3afbea9d8..509e72599c7 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -5,9 +5,12 @@ from typing import Dict from typing import Generator +import py + import pytest from _pytest.compat import TYPE_CHECKING from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Testdir if TYPE_CHECKING: from typing import Type @@ -45,9 +48,12 @@ class A: monkeypatch.undo() # double-undo makes no modification assert A.x == 5 + with pytest.raises(TypeError): + monkeypatch.setattr(A, "y") # type: ignore[call-overload] + class TestSetattrWithImportPath: - def test_string_expression(self, monkeypatch): + def test_string_expression(self, monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr("os.path.abspath", lambda x: "hello2") assert os.path.abspath("123") == "hello2" @@ -64,30 +70,31 @@ def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None: assert _pytest.config.Config == 42 # type: ignore monkeypatch.delattr("_pytest.config.Config") - def test_wrong_target(self, monkeypatch): - pytest.raises(TypeError, lambda: monkeypatch.setattr(None, None)) + def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None: + with pytest.raises(TypeError): + monkeypatch.setattr(None, None) # type: ignore[call-overload] - def test_unknown_import(self, monkeypatch): - pytest.raises(ImportError, lambda: monkeypatch.setattr("unkn123.classx", None)) + def test_unknown_import(self, monkeypatch: MonkeyPatch) -> None: + with pytest.raises(ImportError): + monkeypatch.setattr("unkn123.classx", None) - def test_unknown_attr(self, monkeypatch): - pytest.raises( - AttributeError, lambda: monkeypatch.setattr("os.path.qweqwe", None) - ) + def test_unknown_attr(self, monkeypatch: MonkeyPatch) -> None: + with pytest.raises(AttributeError): + monkeypatch.setattr("os.path.qweqwe", None) def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None: # https://github.com/pytest-dev/pytest/issues/746 monkeypatch.setattr("os.path.qweqwe", 42, raising=False) assert os.path.qweqwe == 42 # type: ignore - def test_delattr(self, monkeypatch): + def test_delattr(self, monkeypatch: MonkeyPatch) -> None: monkeypatch.delattr("os.path.abspath") assert not hasattr(os.path, "abspath") monkeypatch.undo() assert os.path.abspath -def test_delattr(): +def test_delattr() -> None: class A: x = 1 @@ -107,7 +114,7 @@ class A: assert A.x == 1 -def test_setitem(): +def test_setitem() -> None: d = {"x": 1} monkeypatch = MonkeyPatch() monkeypatch.setitem(d, "x", 2) @@ -135,7 +142,7 @@ def test_setitem_deleted_meanwhile() -> None: @pytest.mark.parametrize("before", [True, False]) -def test_setenv_deleted_meanwhile(before): +def test_setenv_deleted_meanwhile(before: bool) -> None: key = "qwpeoip123" if before: os.environ[key] = "world" @@ -167,10 +174,10 @@ def test_delitem() -> None: assert d == {"hello": "world", "x": 1} -def test_setenv(): +def test_setenv() -> None: monkeypatch = MonkeyPatch() with pytest.warns(pytest.PytestWarning): - monkeypatch.setenv("XYZ123", 2) + monkeypatch.setenv("XYZ123", 2) # type: ignore[arg-type] import os assert os.environ["XYZ123"] == "2" @@ -178,7 +185,7 @@ def test_setenv(): assert "XYZ123" not in os.environ -def test_delenv(): +def test_delenv() -> None: name = "xyz1234" assert name not in os.environ monkeypatch = MonkeyPatch() @@ -208,31 +215,28 @@ class TestEnvironWarnings: VAR_NAME = "PYTEST_INTERNAL_MY_VAR" - def test_setenv_non_str_warning(self, monkeypatch): + def test_setenv_non_str_warning(self, monkeypatch: MonkeyPatch) -> None: value = 2 msg = ( "Value of environment variable PYTEST_INTERNAL_MY_VAR type should be str, " "but got 2 (type: int); converted to str implicitly" ) with pytest.warns(pytest.PytestWarning, match=re.escape(msg)): - monkeypatch.setenv(str(self.VAR_NAME), value) + monkeypatch.setenv(str(self.VAR_NAME), value) # type: ignore[arg-type] -def test_setenv_prepend(): +def test_setenv_prepend() -> None: import os monkeypatch = MonkeyPatch() - with pytest.warns(pytest.PytestWarning): - monkeypatch.setenv("XYZ123", 2, prepend="-") - assert os.environ["XYZ123"] == "2" - with pytest.warns(pytest.PytestWarning): - monkeypatch.setenv("XYZ123", 3, prepend="-") + monkeypatch.setenv("XYZ123", "2", prepend="-") + monkeypatch.setenv("XYZ123", "3", prepend="-") assert os.environ["XYZ123"] == "3-2" monkeypatch.undo() assert "XYZ123" not in os.environ -def test_monkeypatch_plugin(testdir): +def test_monkeypatch_plugin(testdir: Testdir) -> None: reprec = testdir.inline_runsource( """ def test_method(monkeypatch): @@ -243,7 +247,7 @@ def test_method(monkeypatch): assert tuple(res) == (1, 0, 0), res -def test_syspath_prepend(mp: MonkeyPatch): +def test_syspath_prepend(mp: MonkeyPatch) -> None: old = list(sys.path) mp.syspath_prepend("world") mp.syspath_prepend("hello") @@ -255,7 +259,7 @@ def test_syspath_prepend(mp: MonkeyPatch): assert sys.path == old -def test_syspath_prepend_double_undo(mp: MonkeyPatch): +def test_syspath_prepend_double_undo(mp: MonkeyPatch) -> None: old_syspath = sys.path[:] try: mp.syspath_prepend("hello world") @@ -267,24 +271,24 @@ def test_syspath_prepend_double_undo(mp: MonkeyPatch): sys.path[:] = old_syspath -def test_chdir_with_path_local(mp: MonkeyPatch, tmpdir): +def test_chdir_with_path_local(mp: MonkeyPatch, tmpdir: py.path.local) -> None: mp.chdir(tmpdir) assert os.getcwd() == tmpdir.strpath -def test_chdir_with_str(mp: MonkeyPatch, tmpdir): +def test_chdir_with_str(mp: MonkeyPatch, tmpdir: py.path.local) -> None: mp.chdir(tmpdir.strpath) assert os.getcwd() == tmpdir.strpath -def test_chdir_undo(mp: MonkeyPatch, tmpdir): +def test_chdir_undo(mp: MonkeyPatch, tmpdir: py.path.local) -> None: cwd = os.getcwd() mp.chdir(tmpdir) mp.undo() assert os.getcwd() == cwd -def test_chdir_double_undo(mp: MonkeyPatch, tmpdir): +def test_chdir_double_undo(mp: MonkeyPatch, tmpdir: py.path.local) -> None: mp.chdir(tmpdir.strpath) mp.undo() tmpdir.chdir() @@ -292,7 +296,7 @@ def test_chdir_double_undo(mp: MonkeyPatch, tmpdir): assert os.getcwd() == tmpdir.strpath -def test_issue185_time_breaks(testdir): +def test_issue185_time_breaks(testdir: Testdir) -> None: testdir.makepyfile( """ import time @@ -310,7 +314,7 @@ def f(): ) -def test_importerror(testdir): +def test_importerror(testdir: Testdir) -> None: p = testdir.mkpydir("package") p.join("a.py").write( textwrap.dedent( @@ -360,7 +364,7 @@ def test_issue156_undo_staticmethod(Sample: "Type[Sample]") -> None: assert Sample.hello() -def test_undo_class_descriptors_delattr(): +def test_undo_class_descriptors_delattr() -> None: class SampleParent: @classmethod def hello(_cls): @@ -387,7 +391,7 @@ class SampleChild(SampleParent): assert original_world == SampleChild.world -def test_issue1338_name_resolving(): +def test_issue1338_name_resolving() -> None: pytest.importorskip("requests") monkeypatch = MonkeyPatch() try: @@ -396,7 +400,7 @@ def test_issue1338_name_resolving(): monkeypatch.undo() -def test_context(): +def test_context() -> None: monkeypatch = MonkeyPatch() import functools @@ -408,7 +412,9 @@ def test_context(): assert inspect.isclass(functools.partial) -def test_syspath_prepend_with_namespace_packages(testdir, monkeypatch): +def test_syspath_prepend_with_namespace_packages( + testdir: Testdir, monkeypatch: MonkeyPatch +) -> None: for dirname in "hello", "world": d = testdir.mkdir(dirname) ns = d.mkdir("ns_pkg") From 7081ed19b892a799e1dea99a7922cab79fefc1df Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 7 Jun 2020 13:05:32 +0300 Subject: [PATCH 367/823] hookspec: type annotate pytest_keyboard_interrupt --- src/_pytest/capture.py | 2 +- src/_pytest/hookspec.py | 6 +++++- src/_pytest/terminal.py | 14 +++++++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 6009e1f67b7..f2beb8d8d6d 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -749,7 +749,7 @@ def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]: yield @pytest.hookimpl(tryfirst=True) - def pytest_keyboard_interrupt(self, excinfo): + def pytest_keyboard_interrupt(self) -> None: self.stop_global_capturing() @pytest.hookimpl(tryfirst=True) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 18a9fb39af9..a6decb03a92 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -19,6 +19,7 @@ import warnings from typing_extensions import Literal + from _pytest.code import ExceptionInfo from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import PytestPluginManager @@ -30,6 +31,7 @@ from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.nodes import Node + from _pytest.outcomes import Exit from _pytest.python import Function from _pytest.python import Metafunc from _pytest.python import Module @@ -761,7 +763,9 @@ def pytest_internalerror(excrepr, excinfo): """ called for internal errors. """ -def pytest_keyboard_interrupt(excinfo): +def pytest_keyboard_interrupt( + excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]", +) -> None: """ called for keyboard interrupt. """ diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 9c2665fb818..da8c5fc9ea4 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -30,6 +30,9 @@ import pytest from _pytest import nodes from _pytest import timing +from _pytest._code import ExceptionInfo +from _pytest._code.code import ExceptionChainRepr +from _pytest._code.code import ReprExceptionInfo from _pytest._io import TerminalWriter from _pytest._io.wcwidth import wcswidth from _pytest.compat import order_preserving_dict @@ -315,6 +318,9 @@ def __init__(self, config: Config, file: Optional[TextIO] = None) -> None: self._show_progress_info = self._determine_show_progress_info() self._collect_report_last_write = None # type: Optional[float] self._already_displayed_warnings = None # type: Optional[int] + self._keyboardinterrupt_memo = ( + None + ) # type: Optional[Union[ReprExceptionInfo, ExceptionChainRepr]] @property def writer(self) -> TerminalWriter: @@ -783,7 +789,7 @@ def pytest_sessionfinish( self.write_sep("!", str(session.shouldfail), red=True) if exitstatus == ExitCode.INTERRUPTED: self._report_keyboardinterrupt() - del self._keyboardinterrupt_memo + self._keyboardinterrupt_memo = None elif session.shouldstop: self.write_sep("!", str(session.shouldstop), red=True) self.summary_stats() @@ -799,15 +805,17 @@ def pytest_terminal_summary(self) -> Generator[None, None, None]: # Display any extra warnings from teardown here (if any). self.summary_warnings() - def pytest_keyboard_interrupt(self, excinfo) -> None: + def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None: self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) def pytest_unconfigure(self) -> None: - if hasattr(self, "_keyboardinterrupt_memo"): + if self._keyboardinterrupt_memo is not None: self._report_keyboardinterrupt() def _report_keyboardinterrupt(self) -> None: excrepr = self._keyboardinterrupt_memo + assert excrepr is not None + assert excrepr.reprcrash is not None msg = excrepr.reprcrash.message self.write_sep("!", msg) if "KeyboardInterrupt" in msg: From 0256cb3aae7221909244d187ed912716fcc5aa5e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 8 Jun 2020 16:08:46 +0300 Subject: [PATCH 368/823] hookspec: type annotate pytest_internalerror Also switch to using ExceptionRepr instead of `Union[ReprExceptionInfo, ExceptionChainRepr]` which is somewhat annoying and less future proof. --- src/_pytest/_code/code.py | 5 +++++ src/_pytest/capture.py | 2 +- src/_pytest/config/__init__.py | 9 +++++++-- src/_pytest/debugging.py | 14 ++++++++++---- src/_pytest/hookspec.py | 11 +++++++++-- src/_pytest/junitxml.py | 3 ++- src/_pytest/resultlog.py | 9 +++++---- src/_pytest/terminal.py | 11 ++++------- 8 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index a40b2347076..121ef3a9b6b 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -928,8 +928,13 @@ def toterminal(self, tw: TerminalWriter) -> None: raise NotImplementedError() +# This class is abstract -- only subclasses are instantiated. @attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ExceptionRepr(TerminalRepr): + # Provided by in subclasses. + reprcrash = None # type: Optional[ReprFileLocation] + reprtraceback = None # type: ReprTraceback + def __attrs_post_init__(self): self.sections = [] # type: List[Tuple[str, str, str]] diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index f2beb8d8d6d..daded6395ee 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -753,7 +753,7 @@ def pytest_keyboard_interrupt(self) -> None: self.stop_global_capturing() @pytest.hookimpl(tryfirst=True) - def pytest_internalerror(self, excinfo): + def pytest_internalerror(self) -> None: self.stop_global_capturing() diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index daccdc6a1d1..2ae9ac84997 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -48,6 +48,7 @@ if TYPE_CHECKING: from typing import Type + from _pytest._code.code import _TracebackStyle from .argparsing import Argument @@ -893,9 +894,13 @@ def pytest_cmdline_parse( return self - def notify_exception(self, excinfo, option=None): + def notify_exception( + self, + excinfo: ExceptionInfo[BaseException], + option: Optional[argparse.Namespace] = None, + ) -> None: if option and getattr(option, "fulltrace", False): - style = "long" + style = "long" # type: _TracebackStyle else: style = "native" excrepr = excinfo.getrepr( diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 3001db4ec63..0567927c0d2 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -2,11 +2,13 @@ import argparse import functools import sys +import types from typing import Generator from typing import Tuple from typing import Union from _pytest import outcomes +from _pytest._code import ExceptionInfo from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import ConftestImportFailure @@ -280,9 +282,10 @@ def pytest_exception_interact( out, err = capman.read_global_capture() sys.stdout.write(out) sys.stdout.write(err) + assert call.excinfo is not None _enter_pdb(node, call.excinfo, report) - def pytest_internalerror(self, excrepr, excinfo) -> None: + def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None: tb = _postmortem_traceback(excinfo) post_mortem(tb) @@ -320,7 +323,9 @@ def maybe_wrap_pytest_function_for_tracing(pyfuncitem): wrap_pytest_function_for_tracing(pyfuncitem) -def _enter_pdb(node: Node, excinfo, rep: BaseReport) -> BaseReport: +def _enter_pdb( + node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport +) -> BaseReport: # XXX we re-use the TerminalReporter's terminalwriter # because this seems to avoid some encoding related troubles # for not completely clear reasons. @@ -349,7 +354,7 @@ def _enter_pdb(node: Node, excinfo, rep: BaseReport) -> BaseReport: return rep -def _postmortem_traceback(excinfo): +def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.TracebackType: from doctest import UnexpectedException if isinstance(excinfo.value, UnexpectedException): @@ -361,10 +366,11 @@ def _postmortem_traceback(excinfo): # Use the underlying exception instead: return excinfo.value.excinfo[2] else: + assert excinfo._excinfo is not None return excinfo._excinfo[2] -def post_mortem(t) -> None: +def post_mortem(t: types.TracebackType) -> None: p = pytestPDB._init_pdb("post_mortem") p.reset() p.interaction(None, t) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index a6decb03a92..1c1726d53b0 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -19,6 +19,7 @@ import warnings from typing_extensions import Literal + from _pytest._code.code import ExceptionRepr from _pytest.code import ExceptionInfo from _pytest.config import Config from _pytest.config import ExitCode @@ -759,8 +760,14 @@ def pytest_doctest_prepare_content(content): # ------------------------------------------------------------------------- -def pytest_internalerror(excrepr, excinfo): - """ called for internal errors. """ +def pytest_internalerror( + excrepr: "ExceptionRepr", excinfo: "ExceptionInfo[BaseException]", +) -> Optional[bool]: + """Called for internal errors. + + Return True to suppress the fallback handling of printing an + INTERNALERROR message directly to sys.stderr. + """ def pytest_keyboard_interrupt( diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index e62bc5235e6..86e8fcf3810 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -26,6 +26,7 @@ from _pytest import deprecated from _pytest import nodes from _pytest import timing +from _pytest._code.code import ExceptionRepr from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import filename_arg @@ -642,7 +643,7 @@ def pytest_collectreport(self, report: TestReport) -> None: else: reporter.append_collect_skipped(report) - def pytest_internalerror(self, excrepr) -> None: + def pytest_internalerror(self, excrepr: ExceptionRepr) -> None: reporter = self.node_reporter("internal") reporter.attrs.update(classname="pytest", name="internal") reporter._add_simple(Junit.error, "internal error", excrepr) diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index c870ef08eae..cd6824abfc5 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -5,6 +5,7 @@ import py +from _pytest._code.code import ExceptionRepr from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.reports import CollectReport @@ -99,9 +100,9 @@ def pytest_collectreport(self, report: CollectReport) -> None: longrepr = "%s:%d: %s" % report.longrepr # type: ignore self.log_outcome(report, code, longrepr) - def pytest_internalerror(self, excrepr): - reprcrash = getattr(excrepr, "reprcrash", None) - path = getattr(reprcrash, "path", None) - if path is None: + def pytest_internalerror(self, excrepr: ExceptionRepr) -> None: + if excrepr.reprcrash is not None: + path = excrepr.reprcrash.path + else: path = "cwd:%s" % py.path.local() self.write_log_entry(path, "!", str(excrepr)) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index da8c5fc9ea4..e89776109a1 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -31,8 +31,7 @@ from _pytest import nodes from _pytest import timing from _pytest._code import ExceptionInfo -from _pytest._code.code import ExceptionChainRepr -from _pytest._code.code import ReprExceptionInfo +from _pytest._code.code import ExceptionRepr from _pytest._io import TerminalWriter from _pytest._io.wcwidth import wcswidth from _pytest.compat import order_preserving_dict @@ -318,9 +317,7 @@ def __init__(self, config: Config, file: Optional[TextIO] = None) -> None: self._show_progress_info = self._determine_show_progress_info() self._collect_report_last_write = None # type: Optional[float] self._already_displayed_warnings = None # type: Optional[int] - self._keyboardinterrupt_memo = ( - None - ) # type: Optional[Union[ReprExceptionInfo, ExceptionChainRepr]] + self._keyboardinterrupt_memo = None # type: Optional[ExceptionRepr] @property def writer(self) -> TerminalWriter: @@ -454,10 +451,10 @@ def _add_stats(self, category: str, items: List) -> None: if set_main_color: self._set_main_color() - def pytest_internalerror(self, excrepr): + def pytest_internalerror(self, excrepr: ExceptionRepr) -> bool: for line in str(excrepr).split("\n"): self.write_line("INTERNALERROR> " + line) - return 1 + return True def pytest_warning_recorded( self, warning_message: warnings.WarningMessage, nodeid: str, From 1cf9405075d889dadae8f31de8b5715f959bcdf9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 12 Jun 2020 15:52:22 +0300 Subject: [PATCH 369/823] Fix some type errors around py.path.local These errors are found using a typed version of py.path.local. --- src/_pytest/assertion/rewrite.py | 8 +++++--- src/_pytest/cacheprovider.py | 2 +- src/_pytest/config/__init__.py | 10 +++++----- src/_pytest/config/findpaths.py | 15 +++++++++------ src/_pytest/logging.py | 2 +- src/_pytest/main.py | 8 ++++---- src/_pytest/nodes.py | 2 +- src/_pytest/python.py | 6 +++--- src/_pytest/terminal.py | 4 ++-- testing/acceptance_test.py | 5 +++-- testing/test_config.py | 3 ++- 11 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index cec0c550195..e77b1b0b861 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -23,6 +23,8 @@ from typing import Tuple from typing import Union +import py + from _pytest._io.saferepr import saferepr from _pytest._version import version from _pytest.assertion import util @@ -177,10 +179,10 @@ def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool: """ if self.session is not None and not self._session_paths_checked: self._session_paths_checked = True - for path in self.session._initialpaths: + for initial_path in self.session._initialpaths: # Make something as c:/projects/my_project/path.py -> # ['c:', 'projects', 'my_project', 'path.py'] - parts = str(path).split(os.path.sep) + parts = str(initial_path).split(os.path.sep) # add 'path' to basenames to be checked. self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0]) @@ -213,7 +215,7 @@ def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool: return True if self.session is not None: - if self.session.isinitpath(fn): + if self.session.isinitpath(py.path.local(fn)): state.trace( "matched test file (was specified on cmdline): {!r}".format(fn) ) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 305a122e9ae..967272ca6ba 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -495,7 +495,7 @@ def pytest_report_header(config: Config) -> Optional[str]: # starting with .., ../.. if sensible try: - displaypath = cachedir.relative_to(config.rootdir) + displaypath = cachedir.relative_to(str(config.rootdir)) except ValueError: displaypath = cachedir return "cachedir: {}".format(displaypath) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 2ae9ac84997..e154f162b07 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -308,10 +308,9 @@ def __init__(self) -> None: self._dirpath2confmods = {} # type: Dict[Any, List[object]] # Maps a py.path.local to a module object. self._conftestpath2mod = {} # type: Dict[Any, object] - self._confcutdir = None + self._confcutdir = None # type: Optional[py.path.local] self._noconftest = False - # Set of py.path.local's. - self._duplicatepaths = set() # type: Set[Any] + self._duplicatepaths = set() # type: Set[py.path.local] self.add_hookspecs(_pytest.hookspec) self.register(self) @@ -945,13 +944,12 @@ def _initini(self, args: Sequence[str]) -> None: ns, unknown_args = self._parser.parse_known_and_unknown_args( args, namespace=copy.copy(self.option) ) - r = determine_setup( + self.rootdir, self.inifile, self.inicfg = determine_setup( ns.inifilename, ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, config=self, ) - self.rootdir, self.inifile, self.inicfg = r self._parser.extra_info["rootdir"] = self.rootdir self._parser.extra_info["inifile"] = self.inifile self._parser.addini("addopts", "extra command line options", "args") @@ -1162,6 +1160,8 @@ def _getini(self, name: str) -> Any: # in this case, we already have a list ready to use # if type == "pathlist": + # TODO: This assert is probably not valid in all cases. + assert self.inifile is not None dp = py.path.local(self.inifile).dirpath() input_values = shlex.split(value) if isinstance(value, str) else value return [dp.join(x, abs=True) for x in input_values] diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 796fa9b0ae0..ae8c5f47f16 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -63,7 +63,7 @@ def load_config_dict_from_file( elif filepath.ext == ".toml": import toml - config = toml.load(filepath) + config = toml.load(str(filepath)) result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) if result is not None: @@ -161,16 +161,18 @@ def determine_setup( args: List[str], rootdir_cmd_arg: Optional[str] = None, config: Optional["Config"] = None, -) -> Tuple[py.path.local, Optional[str], Dict[str, Union[str, List[str]]]]: +) -> Tuple[py.path.local, Optional[py.path.local], Dict[str, Union[str, List[str]]]]: rootdir = None dirs = get_dirs_from_args(args) if inifile: - inicfg = load_config_dict_from_file(py.path.local(inifile)) or {} + inipath_ = py.path.local(inifile) + inipath = inipath_ # type: Optional[py.path.local] + inicfg = load_config_dict_from_file(inipath_) or {} if rootdir_cmd_arg is None: rootdir = get_common_ancestor(dirs) else: ancestor = get_common_ancestor(dirs) - rootdir, inifile, inicfg = locate_config([ancestor]) + rootdir, inipath, inicfg = locate_config([ancestor]) if rootdir is None and rootdir_cmd_arg is None: for possible_rootdir in ancestor.parts(reverse=True): if possible_rootdir.join("setup.py").exists(): @@ -178,7 +180,7 @@ def determine_setup( break else: if dirs != [ancestor]: - rootdir, inifile, inicfg = locate_config(dirs) + rootdir, inipath, inicfg = locate_config(dirs) if rootdir is None: if config is not None: cwd = config.invocation_dir @@ -196,4 +198,5 @@ def determine_setup( rootdir ) ) - return rootdir, inifile, inicfg or {} + assert rootdir is not None + return rootdir, inipath, inicfg or {} diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index ef90c94e862..04bf74b6c57 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -586,7 +586,7 @@ def set_log_path(self, fname: str) -> None: fpath = Path(fname) if not fpath.is_absolute(): - fpath = Path(self._config.rootdir, fpath) + fpath = Path(str(self._config.rootdir), fpath) if not fpath.parent.exists(): fpath.parent.mkdir(exist_ok=True, parents=True) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index a95f2f2e759..096df12dc83 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -439,7 +439,7 @@ def __init__(self, config: Config) -> None: ) # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport] # Dirnames of pkgs with dunder-init files. - self._collection_pkg_roots = {} # type: Dict[py.path.local, Package] + self._collection_pkg_roots = {} # type: Dict[str, Package] self._bestrelpathcache = _bestrelpath_cache( config.rootdir @@ -601,7 +601,7 @@ def _collect( col = self._collectfile(pkginit, handle_dupes=False) if col: if isinstance(col[0], Package): - self._collection_pkg_roots[parent] = col[0] + self._collection_pkg_roots[str(parent)] = col[0] # always store a list in the cache, matchnodes expects it self._collection_node_cache1[col[0].fspath] = [col[0]] @@ -623,8 +623,8 @@ def _collect( for x in self._collectfile(pkginit): yield x if isinstance(x, Package): - self._collection_pkg_roots[dirpath] = x - if dirpath in self._collection_pkg_roots: + self._collection_pkg_roots[str(dirpath)] = x + if str(dirpath) in self._collection_pkg_roots: # Do not collect packages here. continue diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 3757e0b2717..c6c77f529b6 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -393,7 +393,7 @@ def _repr_failure_py( # It will be better to just always display paths relative to invocation_dir, but # this requires a lot of plumbing (#6428). try: - abspath = Path(os.getcwd()) != Path(self.config.invocation_dir) + abspath = Path(os.getcwd()) != Path(str(self.config.invocation_dir)) except OSError: abspath = True diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 4b716c616b6..c527710579b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -656,7 +656,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: parts_ = parts(path.strpath) if any( - pkg_prefix in parts_ and pkg_prefix.join("__init__.py") != path + str(pkg_prefix) in parts_ and pkg_prefix.join("__init__.py") != path for pkg_prefix in pkg_prefixes ): continue @@ -1332,7 +1332,7 @@ def _show_fixtures_per_test(config, session): def get_best_relpath(func): loc = getlocation(func, curdir) - return curdir.bestrelpath(loc) + return curdir.bestrelpath(py.path.local(loc)) def write_fixture(fixture_def): argname = fixture_def.argname @@ -1406,7 +1406,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: ( len(fixturedef.baseid), fixturedef.func.__module__, - curdir.bestrelpath(loc), + curdir.bestrelpath(py.path.local(loc)), fixturedef.argname, fixturedef, ) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index e89776109a1..8c2a3073916 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -380,9 +380,9 @@ def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None: if self.currentfspath is not None and self._show_progress_info: self._write_progress_information_filling_space() self.currentfspath = fspath - fspath = self.startdir.bestrelpath(fspath) + relfspath = self.startdir.bestrelpath(fspath) self._tw.line() - self._tw.write(fspath + " ") + self._tw.write(relfspath + " ") self._tw.write(res, flush=True, **markup) def write_ensure_prefix(self, prefix, extra: str = "", **kwargs) -> None: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 7dfd588a02b..686fe1b981d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -580,8 +580,9 @@ def test_python_pytest_package(self, testdir): assert res.ret == 0 res.stdout.fnmatch_lines(["*1 passed*"]) - def test_equivalence_pytest_pytest(self): - assert pytest.main == py.test.cmdline.main + def test_equivalence_pytest_pydottest(self) -> None: + # Type ignored because `py.test` is not and will not be typed. + assert pytest.main == py.test.cmdline.main # type: ignore[attr-defined] def test_invoke_with_invalid_type(self): with pytest.raises( diff --git a/testing/test_config.py b/testing/test_config.py index 31dfd9fa30a..fc128dd259b 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -629,13 +629,14 @@ def test_inifilename(self, tmpdir): ) with cwd.ensure(dir=True).as_cwd(): config = Config.fromdictargs(option_dict, ()) + inipath = py.path.local(inifile) assert config.args == [str(cwd)] assert config.option.inifilename == inifile assert config.option.capture == "no" # this indicates this is the file used for getting configuration values - assert config.inifile == inifile + assert config.inifile == inipath assert config.inicfg.get("name") == "value" assert config.inicfg.get("should_not_be_set") is None From a5ab7c19fb44cc5177faea95a8f2322af6f2b415 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 12 Jun 2020 16:26:33 +0300 Subject: [PATCH 370/823] config: reject minversion if it's a list instead of a single string Fixes: src/_pytest/config/__init__.py:1071: error: Argument 1 to "Version" has incompatible type "Union[str, List[str]]"; expected "str" [arg-type] --- src/_pytest/config/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index e154f162b07..7dff52c676c 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1067,6 +1067,11 @@ def _checkversion(self): # Imported lazily to improve start-up time. from packaging.version import Version + if not isinstance(minver, str): + raise pytest.UsageError( + "%s: 'minversion' must be a single value" % self.inifile + ) + if Version(minver) > Version(pytest.__version__): raise pytest.UsageError( "%s: 'minversion' requires pytest-%s, actual pytest-%s'" From caa984c02905e06932f68e8b5295f30f270263f3 Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Thu, 11 Jun 2020 20:26:45 +0300 Subject: [PATCH 371/823] Fix exception causes in config/__init__.py --- src/_pytest/config/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c94ea2a9319..a0d36c55a0c 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -641,7 +641,7 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No except ImportError as e: raise ImportError( 'Error importing plugin "{}": {}'.format(modname, str(e.args[0])) - ).with_traceback(e.__traceback__) + ).with_traceback(e.__traceback__) from e except Skipped as e: from _pytest.warnings import _issue_warning_captured @@ -1197,12 +1197,12 @@ def _get_override_ini_value(self, name: str) -> Optional[str]: for ini_config in self._override_ini: try: key, user_ini_value = ini_config.split("=", 1) - except ValueError: + except ValueError as e: raise UsageError( "-o/--override-ini expects option=value style (got: {!r}).".format( ini_config ) - ) + ) from e else: if key == name: value = user_ini_value From 6f8633cc17b6a54329febf81c76157a0d6d39621 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 13 Jun 2020 02:47:15 -0400 Subject: [PATCH 372/823] add in solution barring documentation --- .pre-commit-config.yaml | 12 +++---- src/_pytest/config/__init__.py | 21 ++++++++++--- testing/test_config.py | 57 ++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc371720417..be4b16d6155 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,12 +42,12 @@ repos: - id: setup-cfg-fmt # TODO: when upgrading setup-cfg-fmt this can be removed args: [--max-py-version=3.9] -- repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.780 # NOTE: keep this in sync with setup.cfg. - hooks: - - id: mypy - files: ^(src/|testing/) - args: [] + #- repo: https://github.com/pre-commit/mirrors-mypy + #rev: v0.780 # NOTE: keep this in sync with setup.cfg. + #hooks: + #- id: mypy + #files: ^(src/|testing/) + #args: [] - repo: local hooks: - id: rst diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6e26bf15cf1..887744a0301 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1088,13 +1088,26 @@ def _validate_plugins(self) -> None: if not required_plugins: return + # Imported lazily to improve start-up time. + from packaging.version import Version + from packaging.requirements import InvalidRequirement, Requirement + plugin_info = self.pluginmanager.list_plugin_distinfo() - plugin_dist_names = [dist.project_name for _, dist in plugin_info] + plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info} missing_plugins = [] - for plugin in required_plugins: - if plugin not in plugin_dist_names: - missing_plugins.append(plugin) + for required_plugin in required_plugins: + spec = None + try: + spec = Requirement(required_plugin) + except InvalidRequirement: + missing_plugins.append(required_plugin) + continue + + if spec.name not in plugin_dist_info: + missing_plugins.append(required_plugin) + elif Version(plugin_dist_info[spec.name]) not in spec.specifier: + missing_plugins.append(required_plugin) if missing_plugins: fail( diff --git a/testing/test_config.py b/testing/test_config.py index a10ac41dd9c..420fa06f85a 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -250,6 +250,63 @@ def test_invalid_ini_keys( ), ( """ + [pytest] + required_plugins = pytest-xdist + """, + "", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist==1.32.0 + """, + "", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist~=1.32.0 pytest-xdist==1.32.0 pytest-xdist!=0.0.1 pytest-xdist<=99.99.0 + pytest-xdist>=1.32.0 pytest-xdist<9.9.9 pytest-xdist>1.30.0 pytest-xdist===1.32.0 + """, + "", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist>9.9.9 pytest-xdist==1.32.0 pytest-xdist==8.8.8 + """, + "Missing required plugins: pytest-xdist==8.8.8, pytest-xdist>9.9.9", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist==aegsrgrsgs + """, + "Missing required plugins: pytest-xdist==aegsrgrsgs", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist==-1 + """, + "Missing required plugins: pytest-xdist==-1", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist== pytest-xdist<= + """, + "Missing required plugins: pytest-xdist<=, pytest-xdist==", + ), + ( + """ + [pytest] + required_plugins = pytest-xdist= pytest-xdist< + """, + "Missing required plugins: pytest-xdist<, pytest-xdist=", + ), + ( + """ [some_other_header] required_plugins = wont be triggered [pytest] From 31512197851e556f5ed8bb964d69eef6398294e4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 13 Jun 2020 10:24:44 -0300 Subject: [PATCH 373/823] assertoutcomes() only accepts plural forms Fix #6505 --- changelog/6505.breaking.rst | 20 ++++++++++++++++++++ src/_pytest/pytester.py | 37 ++++++++++++++++++++++++++++--------- testing/python/fixtures.py | 2 +- testing/test_pytester.py | 33 +++++++++++++++++++++++++++++++-- 4 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 changelog/6505.breaking.rst diff --git a/changelog/6505.breaking.rst b/changelog/6505.breaking.rst new file mode 100644 index 00000000000..164b69485a0 --- /dev/null +++ b/changelog/6505.breaking.rst @@ -0,0 +1,20 @@ +``Testdir.run().parseoutcomes()`` now always returns the parsed nouns in plural form. + +Originally ``parseoutcomes()`` would always returns the nouns in plural form, but a change +meant to improve the terminal summary by using singular form single items (``1 warning`` or ``1 error``) +caused an unintended regression by changing the keys returned by ``parseoutcomes()``. + +Now the API guarantees to always return the plural form, so calls like this: + +.. code-block:: python + + result = testdir.runpytest() + result.assert_outcomes(error=1) + +Need to be changed to: + + +.. code-block:: python + + result = testdir.runpytest() + result.assert_outcomes(errors=1) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 2913c60654a..cf3dbd2011d 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -452,28 +452,47 @@ def __repr__(self) -> str: ) def parseoutcomes(self) -> Dict[str, int]: - """Return a dictionary of outcomestring->num from parsing the terminal + """Return a dictionary of outcome noun -> count from parsing the terminal output that the test process produced. + The returned nouns will always be in plural form:: + + ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== + + Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}`` + """ + return self.parse_summary_nouns(self.outlines) + + @classmethod + def parse_summary_nouns(cls, lines) -> Dict[str, int]: + """Extracts the nouns from a pytest terminal summary line. + + It always returns the plural noun for consistency:: + + ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== + + Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}`` """ - for line in reversed(self.outlines): + for line in reversed(lines): if rex_session_duration.search(line): outcomes = rex_outcome.findall(line) ret = {noun: int(count) for (count, noun) in outcomes} break else: raise ValueError("Pytest terminal summary report not found") - if "errors" in ret: - assert "error" not in ret - ret["error"] = ret.pop("errors") - return ret + + to_plural = { + "warning": "warnings", + "error": "errors", + } + return {to_plural.get(k, k): v for k, v in ret.items()} def assert_outcomes( self, passed: int = 0, skipped: int = 0, failed: int = 0, - error: int = 0, + errors: int = 0, xpassed: int = 0, xfailed: int = 0, ) -> None: @@ -487,7 +506,7 @@ def assert_outcomes( "passed": d.get("passed", 0), "skipped": d.get("skipped", 0), "failed": d.get("failed", 0), - "error": d.get("error", 0), + "errors": d.get("errors", 0), "xpassed": d.get("xpassed", 0), "xfailed": d.get("xfailed", 0), } @@ -495,7 +514,7 @@ def assert_outcomes( "passed": passed, "skipped": skipped, "failed": failed, - "error": error, + "errors": errors, "xpassed": xpassed, "xfailed": xfailed, } diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 353ce46cd6b..e4351a81666 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4342,6 +4342,6 @@ def test_fixt(custom): ) expected = "E ValueError: custom did not yield a value" result = testdir.runpytest() - result.assert_outcomes(error=1) + result.assert_outcomes(errors=1) result.stdout.fnmatch_lines([expected]) assert result.ret == ExitCode.TESTS_FAILED diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 1d332145504..d0afb40b07d 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -763,9 +763,38 @@ def test_error2(bad_fixture): """ ) result = testdir.runpytest(str(p1)) - result.assert_outcomes(error=2) + result.assert_outcomes(errors=2) - assert result.parseoutcomes() == {"error": 2} + assert result.parseoutcomes() == {"errors": 2} + + +def test_parse_summary_line_always_plural(): + """Parsing summaries always returns plural nouns (#6505)""" + lines = [ + "some output 1", + "some output 2", + "======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====", + "done.", + ] + assert pytester.RunResult.parse_summary_nouns(lines) == { + "errors": 1, + "failed": 1, + "passed": 1, + "warnings": 1, + } + + lines = [ + "some output 1", + "some output 2", + "======= 1 failed, 1 passed, 2 warnings, 2 errors in 0.13s ====", + "done.", + ] + assert pytester.RunResult.parse_summary_nouns(lines) == { + "errors": 2, + "failed": 1, + "passed": 1, + "warnings": 2, + } def test_makefile_joins_absolute_path(testdir: Testdir) -> None: From f8a8bdbeb050533615be7ef517a0b5ca027bb5f4 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 13 Jun 2020 09:55:55 -0400 Subject: [PATCH 374/823] remove pre-commit change --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be4b16d6155..dc371720417 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,12 +42,12 @@ repos: - id: setup-cfg-fmt # TODO: when upgrading setup-cfg-fmt this can be removed args: [--max-py-version=3.9] - #- repo: https://github.com/pre-commit/mirrors-mypy - #rev: v0.780 # NOTE: keep this in sync with setup.cfg. - #hooks: - #- id: mypy - #files: ^(src/|testing/) - #args: [] +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.780 # NOTE: keep this in sync with setup.cfg. + hooks: + - id: mypy + files: ^(src/|testing/) + args: [] - repo: local hooks: - id: rst From 8a022c0ce3c76176961e07137100bee4d07f7572 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 13 Jun 2020 09:57:13 -0400 Subject: [PATCH 375/823] test to make sure precommit is fixed --- src/_pytest/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 116383a7386..a124b8f5f1f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1101,7 +1101,7 @@ def _validate_plugins(self) -> None: plugin_info = self.pluginmanager.list_plugin_distinfo() plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info} - missing_plugins = [] + missing_plugins = ["a"] for required_plugin in required_plugins: spec = None try: From ab6dacf1d1e1ff0c5be70a3c5f48e63168168721 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 13 Jun 2020 11:29:01 -0300 Subject: [PATCH 376/823] Introduce --import-mode=importlib (#7246) Fix #5821 Co-authored-by: Ran Benita --- changelog/7245.feature.rst | 14 ++ doc/en/goodpractices.rst | 23 +++- doc/en/pythonpath.rst | 64 ++++++++- src/_pytest/_code/code.py | 5 +- src/_pytest/compat.py | 36 +++++ src/_pytest/config/__init__.py | 40 ++++-- src/_pytest/doctest.py | 7 +- src/_pytest/main.py | 8 ++ src/_pytest/nodes.py | 4 +- src/_pytest/pathlib.py | 138 +++++++++++++++++++ src/_pytest/python.py | 15 +- testing/acceptance_test.py | 5 +- testing/python/collect.py | 10 +- testing/python/fixtures.py | 4 +- testing/test_collection.py | 80 +++++++++++ testing/test_compat.py | 59 ++++++++ testing/test_conftest.py | 42 +++--- testing/test_pathlib.py | 242 ++++++++++++++++++++++++++++++++- testing/test_pluginmanager.py | 8 +- 19 files changed, 734 insertions(+), 70 deletions(-) create mode 100644 changelog/7245.feature.rst diff --git a/changelog/7245.feature.rst b/changelog/7245.feature.rst new file mode 100644 index 00000000000..05c3a6c0469 --- /dev/null +++ b/changelog/7245.feature.rst @@ -0,0 +1,14 @@ +New ``--import-mode=importlib`` option that uses `importlib `__ to import test modules. + +Traditionally pytest used ``__import__`` while changing ``sys.path`` to import test modules (which +also changes ``sys.modules`` as a side-effect), which works but has a number of drawbacks, like requiring test modules +that don't live in packages to have unique names (as they need to reside under a unique name in ``sys.modules``). + +``--import-mode=importlib`` uses more fine grained import mechanisms from ``importlib`` which don't +require pytest to change ``sys.path`` or ``sys.modules`` at all, eliminating much of the drawbacks +of the previous mode. + +We intend to make ``--import-mode=importlib`` the default in future versions, so users are encouraged +to try the new mode and provide feedback (both positive or negative) in issue `#7245 `__. + +You can read more about this option in `the documentation `__. diff --git a/doc/en/goodpractices.rst b/doc/en/goodpractices.rst index 16b41eda4d8..ee5674fd6d8 100644 --- a/doc/en/goodpractices.rst +++ b/doc/en/goodpractices.rst @@ -91,7 +91,8 @@ This has the following benefits: See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and ``python -m pytest``. -Note that using this scheme your test files must have **unique names**, because +Note that this scheme has a drawback if you are using ``prepend`` :ref:`import mode ` +(which is the default): your test files must have **unique names**, because ``pytest`` will import them as *top-level* modules since there are no packages to derive a full package name from. In other words, the test files in the example above will be imported as ``test_app`` and ``test_view`` top-level modules by adding ``tests/`` to @@ -118,9 +119,12 @@ Now pytest will load the modules as ``tests.foo.test_view`` and ``tests.bar.test you to have modules with the same name. But now this introduces a subtle problem: in order to load the test modules from the ``tests`` directory, pytest prepends the root of the repository to ``sys.path``, which adds the side-effect that now ``mypkg`` is also importable. + This is problematic if you are using a tool like `tox`_ to test your package in a virtual environment, because you want to test the *installed* version of your package, not the local code from the repository. +.. _`src-layout`: + In this situation, it is **strongly** suggested to use a ``src`` layout where application root package resides in a sub-directory of your root: @@ -145,6 +149,15 @@ sub-directory of your root: This layout prevents a lot of common pitfalls and has many benefits, which are better explained in this excellent `blog post by Ionel Cristian Mărieș `_. +.. note:: + The new ``--import-mode=importlib`` (see :ref:`import-modes`) doesn't have + any of the drawbacks above because ``sys.path`` and ``sys.modules`` are not changed when importing + test modules, so users that run + into this issue are strongly encouraged to try it and report if the new option works well for them. + + The ``src`` directory layout is still strongly recommended however. + + Tests as part of application code ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -190,8 +203,8 @@ Note that this layout also works in conjunction with the ``src`` layout mentione .. note:: - If ``pytest`` finds an "a/b/test_module.py" test file while - recursing into the filesystem it determines the import name + In ``prepend`` and ``append`` import-modes, if pytest finds a ``"a/b/test_module.py"`` + test file while recursing into the filesystem it determines the import name as follows: * determine ``basedir``: this is the first "upward" (towards the root) @@ -212,6 +225,10 @@ Note that this layout also works in conjunction with the ``src`` layout mentione from each other and thus deriving a canonical import name helps to avoid surprises such as a test module getting imported twice. + With ``--import-mode=importlib`` things are less convoluted because + pytest doesn't need to change ``sys.path`` or ``sys.modules``, making things + much less surprising. + .. _`virtualenv`: https://pypi.org/project/virtualenv/ .. _`buildout`: http://www.buildout.org/ diff --git a/doc/en/pythonpath.rst b/doc/en/pythonpath.rst index f2c86fab967..b8f4de9d95b 100644 --- a/doc/en/pythonpath.rst +++ b/doc/en/pythonpath.rst @@ -3,11 +3,65 @@ pytest import mechanisms and ``sys.path``/``PYTHONPATH`` ======================================================== -Here's a list of scenarios where pytest may need to change ``sys.path`` in order -to import test modules or ``conftest.py`` files. +.. _`import-modes`: + +Import modes +------------ + +pytest as a testing framework needs to import test modules and ``conftest.py`` files for execution. + +Importing files in Python (at least until recently) is a non-trivial processes, often requiring +changing `sys.path `__. Some aspects of the +import process can be controlled through the ``--import-mode`` command-line flag, which can assume +these values: + +* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning* + of ``sys.path`` if not already there, and then imported with the `__import__ `__ builtin. + + This requires test module names to be unique when the test directory tree is not arranged in + packages, because the modules will put in ``sys.modules`` after importing. + + This is the classic mechanism, dating back from the time Python 2 was still supported. + +* ``append``: the directory containing each module is appended to the end of ``sys.path`` if not already + there, and imported with ``__import__``. + + This better allows to run test modules against installed versions of a package even if the + package under test has the same import root. For example: + + :: + + testing/__init__.py + testing/test_pkg_under_test.py + pkg_under_test/ + + the tests will run against the installed version + of ``pkg_under_test`` when ``--import-mode=append`` is used whereas + with ``prepend`` they would pick up the local version. This kind of confusion is why + we advocate for using :ref:`src ` layouts. + + Same as ``prepend``, requires test module names to be unique when the test directory tree is + not arranged in packages, because the modules will put in ``sys.modules`` after importing. + +* ``importlib``: new in pytest-6.0, this mode uses `importlib `__ to import test modules. This gives full control over the import process, and doesn't require + changing ``sys.path`` or ``sys.modules`` at all. + + For this reason this doesn't require test module names to be unique at all, but also makes test + modules non-importable by each other. This was made possible in previous modes, for tests not residing + in Python packages, because of the side-effects of changing ``sys.path`` and ``sys.modules`` + mentioned above. Users which require this should turn their tests into proper packages instead. + + We intend to make ``importlib`` the default in future releases. + +``prepend`` and ``append`` import modes scenarios +------------------------------------------------- + +Here's a list of scenarios when using ``prepend`` or ``append`` import modes where pytest needs to +change ``sys.path`` in order to import test modules or ``conftest.py`` files, and the issues users +might encounter because of that. Test modules / ``conftest.py`` files inside packages ----------------------------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Consider this file and directory layout:: @@ -28,8 +82,6 @@ When executing: pytest root/ - - pytest will find ``foo/bar/tests/test_foo.py`` and realize it is part of a package given that there's an ``__init__.py`` file in the same folder. It will then search upwards until it can find the last folder which still contains an ``__init__.py`` file in order to find the package *root* (in @@ -44,7 +96,7 @@ and allow test modules to have duplicated names. This is also discussed in detai :ref:`test discovery`. Standalone test modules / ``conftest.py`` files ------------------------------------------------ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Consider this file and directory layout:: diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 121ef3a9b6b..65e5aa6d540 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1204,7 +1204,10 @@ def getfslineno(obj: Any) -> Tuple[Union[str, py.path.local], int]: def filter_traceback(entry: TracebackEntry) -> bool: - """Return True if a TracebackEntry instance should be removed from tracebacks: + """Return True if a TracebackEntry instance should be included in tracebacks. + + We hide traceback entries of: + * dynamically generated code (no code to show up for it); * internal traceback from pytest or its internal libraries, py and pluggy. """ diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 84f9609a7db..cd7dca7197a 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -33,6 +33,7 @@ if TYPE_CHECKING: + from typing import NoReturn from typing import Type from typing_extensions import Final @@ -401,3 +402,38 @@ def __get__(self, instance, owner=None): # noqa: F811 from collections import OrderedDict order_preserving_dict = OrderedDict + + +# Perform exhaustiveness checking. +# +# Consider this example: +# +# MyUnion = Union[int, str] +# +# def handle(x: MyUnion) -> int { +# if isinstance(x, int): +# return 1 +# elif isinstance(x, str): +# return 2 +# else: +# raise Exception('unreachable') +# +# Now suppose we add a new variant: +# +# MyUnion = Union[int, str, bytes] +# +# After doing this, we must remember ourselves to go and update the handle +# function to handle the new variant. +# +# With `assert_never` we can do better: +# +# // throw new Error('unreachable'); +# return assert_never(x) +# +# Now, if we forget to handle the new variant, the type-checker will emit a +# compile-time error, instead of the runtime error we would have gotten +# previously. +# +# This also work for Enums (if you use `is` to compare) and Literals. +def assert_never(value: "NoReturn") -> "NoReturn": + assert False, "Unhandled value: {} ({})".format(value, type(value).__name__) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index b7796811061..400acb51f55 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -41,6 +41,7 @@ from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import import_path from _pytest.pathlib import Path from _pytest.store import Store from _pytest.warning_types import PytestConfigWarning @@ -98,6 +99,15 @@ def __str__(self): ) +def filter_traceback_for_conftest_import_failure(entry) -> bool: + """filters tracebacks entries which point to pytest internals or importlib. + + Make a special case for importlib because we use it to import test modules and conftest files + in _pytest.pathlib.import_path. + """ + return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep) + + def main(args=None, plugins=None) -> Union[int, ExitCode]: """ return exit code, after performing an in-process test run. @@ -115,7 +125,9 @@ def main(args=None, plugins=None) -> Union[int, ExitCode]: tw.line( "ImportError while loading conftest '{e.path}'.".format(e=e), red=True ) - exc_info.traceback = exc_info.traceback.filter(filter_traceback) + exc_info.traceback = exc_info.traceback.filter( + filter_traceback_for_conftest_import_failure + ) exc_repr = ( exc_info.getrepr(style="short", chain=False) if exc_info.traceback @@ -450,21 +462,21 @@ def _set_initial_conftests(self, namespace): path = path[:i] anchor = current.join(path, abs=1) if anchor.exists(): # we found some file object - self._try_load_conftest(anchor) + self._try_load_conftest(anchor, namespace.importmode) foundanchor = True if not foundanchor: - self._try_load_conftest(current) + self._try_load_conftest(current, namespace.importmode) - def _try_load_conftest(self, anchor): - self._getconftestmodules(anchor) + def _try_load_conftest(self, anchor, importmode): + self._getconftestmodules(anchor, importmode) # let's also consider test* subdirs if anchor.check(dir=1): for x in anchor.listdir("test*"): if x.check(dir=1): - self._getconftestmodules(x) + self._getconftestmodules(x, importmode) @lru_cache(maxsize=128) - def _getconftestmodules(self, path): + def _getconftestmodules(self, path, importmode): if self._noconftest: return [] @@ -482,13 +494,13 @@ def _getconftestmodules(self, path): continue conftestpath = parent.join("conftest.py") if conftestpath.isfile(): - mod = self._importconftest(conftestpath) + mod = self._importconftest(conftestpath, importmode) clist.append(mod) self._dirpath2confmods[directory] = clist return clist - def _rget_with_confmod(self, name, path): - modules = self._getconftestmodules(path) + def _rget_with_confmod(self, name, path, importmode): + modules = self._getconftestmodules(path, importmode) for mod in reversed(modules): try: return mod, getattr(mod, name) @@ -496,7 +508,7 @@ def _rget_with_confmod(self, name, path): continue raise KeyError(name) - def _importconftest(self, conftestpath): + def _importconftest(self, conftestpath, importmode): # Use a resolved Path object as key to avoid loading the same conftest twice # with build systems that create build directories containing # symlinks to actual files. @@ -512,7 +524,7 @@ def _importconftest(self, conftestpath): _ensure_removed_sysmodule(conftestpath.purebasename) try: - mod = conftestpath.pyimport() + mod = import_path(conftestpath, mode=importmode) except Exception as e: raise ConftestImportFailure(conftestpath, sys.exc_info()) from e @@ -1213,7 +1225,9 @@ def _getini(self, name: str) -> Any: def _getconftest_pathlist(self, name, path): try: - mod, relroots = self.pluginmanager._rget_with_confmod(name, path) + mod, relroots = self.pluginmanager._rget_with_confmod( + name, path, self.getoption("importmode") + ) except KeyError: return None modpath = py.path.local(mod.__file__).dirpath() diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 7aaacb481c2..181c66b95ff 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -33,6 +33,7 @@ from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureRequest from _pytest.outcomes import OutcomeException +from _pytest.pathlib import import_path from _pytest.python_api import approx from _pytest.warning_types import PytestWarning @@ -530,10 +531,12 @@ def _find( ) if self.fspath.basename == "conftest.py": - module = self.config.pluginmanager._importconftest(self.fspath) + module = self.config.pluginmanager._importconftest( + self.fspath, self.config.getoption("importmode") + ) else: try: - module = self.fspath.pyimport() + module = import_path(self.fspath) except ImportError: if self.config.getvalue("doctest_ignore_import_errors"): pytest.skip("unable to import module %r" % self.fspath) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 2ec9046b0a9..b7a3a958a31 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -173,6 +173,14 @@ def pytest_addoption(parser: Parser) -> None: default=False, help="Don't ignore tests in a local virtualenv directory", ) + group.addoption( + "--import-mode", + default="prepend", + choices=["prepend", "append", "importlib"], + dest="importmode", + help="prepend/append to sys.path when importing test modules and conftest files, " + "default is to prepend.", + ) group = parser.getgroup("debugconfig", "test session debugging and configuration") group.addoption( diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index c6c77f529b6..4c7aa1bcd2c 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -547,7 +547,9 @@ def _gethookproxy(self, fspath: py.path.local): # check if we have the common case of running # hooks with all conftest.py files pm = self.config.pluginmanager - my_conftestmodules = pm._getconftestmodules(fspath) + my_conftestmodules = pm._getconftestmodules( + fspath, self.config.getoption("importmode") + ) remove_mods = pm._conftest_plugins.difference(my_conftestmodules) if remove_mods: # one or more conftests are not in use at this fspath diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 98ec936a119..66ae9a51dd1 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -1,24 +1,31 @@ import atexit import contextlib import fnmatch +import importlib.util import itertools import os import shutil import sys import uuid import warnings +from enum import Enum from functools import partial from os.path import expanduser from os.path import expandvars from os.path import isabs from os.path import sep from posixpath import sep as posix_sep +from types import ModuleType from typing import Iterable from typing import Iterator +from typing import Optional from typing import Set from typing import TypeVar from typing import Union +import py + +from _pytest.compat import assert_never from _pytest.outcomes import skip from _pytest.warning_types import PytestWarning @@ -413,3 +420,134 @@ def symlink_or_skip(src, dst, **kwargs): os.symlink(str(src), str(dst), **kwargs) except OSError as e: skip("symlinks not supported: {}".format(e)) + + +class ImportMode(Enum): + """Possible values for `mode` parameter of `import_path`""" + + prepend = "prepend" + append = "append" + importlib = "importlib" + + +class ImportPathMismatchError(ImportError): + """Raised on import_path() if there is a mismatch of __file__'s. + + This can happen when `import_path` is called multiple times with different filenames that has + the same basename but reside in packages + (for example "/tests1/test_foo.py" and "/tests2/test_foo.py"). + """ + + +def import_path( + p: Union[str, py.path.local, Path], + *, + mode: Union[str, ImportMode] = ImportMode.prepend +) -> ModuleType: + """ + Imports and returns a module from the given path, which can be a file (a module) or + a directory (a package). + + The import mechanism used is controlled by the `mode` parameter: + + * `mode == ImportMode.prepend`: the directory containing the module (or package, taking + `__init__.py` files into account) will be put at the *start* of `sys.path` before + being imported with `__import__. + + * `mode == ImportMode.append`: same as `prepend`, but the directory will be appended + to the end of `sys.path`, if not already in `sys.path`. + + * `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib` + to import the module, which avoids having to use `__import__` and muck with `sys.path` + at all. It effectively allows having same-named test modules in different places. + + :raise ImportPathMismatchError: if after importing the given `path` and the module `__file__` + are different. Only raised in `prepend` and `append` modes. + """ + mode = ImportMode(mode) + + path = Path(p) + + if not path.exists(): + raise ImportError(path) + + if mode is ImportMode.importlib: + module_name = path.stem + + for meta_importer in sys.meta_path: + spec = meta_importer.find_spec(module_name, [str(path.parent)]) + if spec is not None: + break + else: + spec = importlib.util.spec_from_file_location(module_name, str(path)) + + if spec is None: + raise ImportError( + "Can't find module {} at location {}".format(module_name, str(path)) + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) # type: ignore[union-attr] + return mod + + pkg_path = resolve_package_path(path) + if pkg_path is not None: + pkg_root = pkg_path.parent + names = list(path.with_suffix("").relative_to(pkg_root).parts) + if names[-1] == "__init__": + names.pop() + module_name = ".".join(names) + else: + pkg_root = path.parent + module_name = path.stem + + # change sys.path permanently: restoring it at the end of this function would cause surprising + # problems because of delayed imports: for example, a conftest.py file imported by this function + # might have local imports, which would fail at runtime if we restored sys.path. + if mode is ImportMode.append: + if str(pkg_root) not in sys.path: + sys.path.append(str(pkg_root)) + elif mode is ImportMode.prepend: + if str(pkg_root) != sys.path[0]: + sys.path.insert(0, str(pkg_root)) + else: + assert_never(mode) + + importlib.import_module(module_name) + + mod = sys.modules[module_name] + if path.name == "__init__.py": + return mod + + ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "") + if ignore != "1": + module_file = mod.__file__ + if module_file.endswith((".pyc", ".pyo")): + module_file = module_file[:-1] + if module_file.endswith(os.path.sep + "__init__.py"): + module_file = module_file[: -(len(os.path.sep + "__init__.py"))] + + try: + is_same = os.path.samefile(str(path), module_file) + except FileNotFoundError: + is_same = False + + if not is_same: + raise ImportPathMismatchError(module_name, module_file, path) + + return mod + + +def resolve_package_path(path: Path) -> Optional[Path]: + """Return the Python package path by looking for the last + directory upwards which still contains an __init__.py. + Return None if it can not be determined. + """ + result = None + for parent in itertools.chain((path,), path.parents): + if parent.is_dir(): + if not parent.joinpath("__init__.py").is_file(): + break + if not parent.name.isidentifier(): + break + result = parent + return result diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c527710579b..bf45b8830e4 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -59,6 +59,8 @@ from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail from _pytest.outcomes import skip +from _pytest.pathlib import import_path +from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import parts from _pytest.reports import TerminalRepr from _pytest.warning_types import PytestCollectionWarning @@ -115,15 +117,6 @@ def pytest_addoption(parser: Parser) -> None: "side effects(use at your own risk)", ) - group.addoption( - "--import-mode", - default="prepend", - choices=["prepend", "append"], - dest="importmode", - help="prepend/append to sys.path when importing test modules, " - "default is to prepend.", - ) - def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.showfixtures: @@ -557,10 +550,10 @@ def _importtestmodule(self): # we assume we are only called once per module importmode = self.config.getoption("--import-mode") try: - mod = self.fspath.pyimport(ensuresyspath=importmode) + mod = import_path(self.fspath, mode=importmode) except SyntaxError: raise self.CollectError(ExceptionInfo.from_current().getrepr(style="short")) - except self.fspath.ImportMismatchError as e: + except ImportPathMismatchError as e: raise self.CollectError( "import file mismatch:\n" "imported module %r has this __file__ attribute:\n" diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 686fe1b981d..d8f7a501a01 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -147,7 +147,8 @@ def my_dists(): else: assert loaded == ["myplugin1", "myplugin2", "mycov"] - def test_assertion_magic(self, testdir): + @pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"]) + def test_assertion_rewrite(self, testdir, import_mode): p = testdir.makepyfile( """ def test_this(): @@ -155,7 +156,7 @@ def test_this(): assert x """ ) - result = testdir.runpytest(p) + result = testdir.runpytest(p, "--import-mode={}".format(import_mode)) result.stdout.fnmatch_lines(["> assert x", "E assert 0"]) assert result.ret == 1 diff --git a/testing/python/collect.py b/testing/python/collect.py index 7824ceff138..e98a21f1cc5 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,4 +1,3 @@ -import os import sys import textwrap from typing import Any @@ -109,11 +108,10 @@ def test_show_traceback_import_error(self, testdir, verbose): assert result.ret == 2 stdout = result.stdout.str() - for name in ("_pytest", os.path.join("py", "_path")): - if verbose == 2: - assert name in stdout - else: - assert name not in stdout + if verbose == 2: + assert "_pytest" in stdout + else: + assert "_pytest" not in stdout def test_show_traceback_import_error_unicode(self, testdir): """Check test modules collected which raise ImportError with unicode messages diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 353ce46cd6b..a34478675ca 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1894,7 +1894,9 @@ def test_2(self): reprec = testdir.inline_run("-v", "-s", confcut) reprec.assertoutcome(passed=8) config = reprec.getcalls("pytest_unconfigure")[0].config - values = config.pluginmanager._getconftestmodules(p)[0].values + values = config.pluginmanager._getconftestmodules(p, importmode="prepend")[ + 0 + ].values assert values == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2 def test_scope_ordering(self, testdir): diff --git a/testing/test_collection.py b/testing/test_collection.py index 6644881ea44..d7a9b0439aa 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1342,3 +1342,83 @@ def from_parent(cls, parent, *, fspath, x): parent=request.session, fspath=tmpdir / "foo", x=10 ) assert collector.x == 10 + + +class TestImportModeImportlib: + def test_collect_duplicate_names(self, testdir): + """--import-mode=importlib can import modules with same names that are not in packages.""" + testdir.makepyfile( + **{ + "tests_a/test_foo.py": "def test_foo1(): pass", + "tests_b/test_foo.py": "def test_foo2(): pass", + } + ) + result = testdir.runpytest("-v", "--import-mode=importlib") + result.stdout.fnmatch_lines( + [ + "tests_a/test_foo.py::test_foo1 *", + "tests_b/test_foo.py::test_foo2 *", + "* 2 passed in *", + ] + ) + + def test_conftest(self, testdir): + """Directory containing conftest modules are not put in sys.path as a side-effect of + importing them.""" + tests_dir = testdir.tmpdir.join("tests") + testdir.makepyfile( + **{ + "tests/conftest.py": "", + "tests/test_foo.py": """ + import sys + def test_check(): + assert r"{tests_dir}" not in sys.path + """.format( + tests_dir=tests_dir + ), + } + ) + result = testdir.runpytest("-v", "--import-mode=importlib") + result.stdout.fnmatch_lines(["* 1 passed in *"]) + + def setup_conftest_and_foo(self, testdir): + """Setup a tests folder to be used to test if modules in that folder can be imported + due to side-effects of --import-mode or not.""" + testdir.makepyfile( + **{ + "tests/conftest.py": "", + "tests/foo.py": """ + def foo(): return 42 + """, + "tests/test_foo.py": """ + def test_check(): + from foo import foo + assert foo() == 42 + """, + } + ) + + def test_modules_importable_as_side_effect(self, testdir): + """In import-modes `prepend` and `append`, we are able to import modules from folders + containing conftest.py files due to the side effect of changing sys.path.""" + self.setup_conftest_and_foo(testdir) + result = testdir.runpytest("-v", "--import-mode=prepend") + result.stdout.fnmatch_lines(["* 1 passed in *"]) + + def test_modules_not_importable_as_side_effect(self, testdir): + """In import-mode `importlib`, modules in folders containing conftest.py are not + importable, as don't change sys.path or sys.modules as side effect of importing + the conftest.py file. + """ + self.setup_conftest_and_foo(testdir) + result = testdir.runpytest("-v", "--import-mode=importlib") + exc_name = ( + "ModuleNotFoundError" if sys.version_info[:2] > (3, 5) else "ImportError" + ) + result.stdout.fnmatch_lines( + [ + "*{}: No module named 'foo'".format(exc_name), + "tests?test_foo.py:2: {}".format(exc_name), + "* 1 failed in *", + ] + ) diff --git a/testing/test_compat.py b/testing/test_compat.py index 45468b5f8dc..5debe87a3ed 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -1,16 +1,23 @@ +import enum import sys from functools import partial from functools import wraps +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 from _pytest.compat import safe_isclass +from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import OutcomeException +if TYPE_CHECKING: + from typing_extensions import Literal + def test_is_generator(): def zap(): @@ -205,3 +212,55 @@ def prop(self) -> int: assert ncalls == 1 assert c2.prop == 2 assert c1.prop == 1 + + +def test_assert_never_union() -> None: + x = 10 # type: Union[int, str] + + if isinstance(x, int): + pass + else: + with pytest.raises(AssertionError): + assert_never(x) # type: ignore[arg-type] + + if isinstance(x, int): + pass + elif isinstance(x, str): + pass + else: + assert_never(x) + + +def test_assert_never_enum() -> None: + E = enum.Enum("E", "a b") + x = E.a # type: E + + if x is E.a: + pass + else: + with pytest.raises(AssertionError): + assert_never(x) # type: ignore[arg-type] + + if x is E.a: + pass + elif x is E.b: + pass + else: + assert_never(x) + + +def test_assert_never_literal() -> None: + x = "a" # type: Literal["a", "b"] + + if x == "a": + pass + else: + with pytest.raises(AssertionError): + assert_never(x) # type: ignore[arg-type] + + if x == "a": + pass + elif x == "b": + pass + else: + assert_never(x) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 0df303bc7cb..724e6f464cb 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -23,6 +23,7 @@ def __init__(self): self.confcutdir = str(confcutdir) self.noconftest = False self.pyargs = False + self.importmode = "prepend" conftest._set_initial_conftests(Namespace()) @@ -43,35 +44,38 @@ def basedir(self, request, tmpdir_factory): def test_basic_init(self, basedir): conftest = PytestPluginManager() p = basedir.join("adir") - assert conftest._rget_with_confmod("a", p)[1] == 1 + assert conftest._rget_with_confmod("a", p, importmode="prepend")[1] == 1 def test_immediate_initialiation_and_incremental_are_the_same(self, basedir): conftest = PytestPluginManager() assert not len(conftest._dirpath2confmods) - conftest._getconftestmodules(basedir) + conftest._getconftestmodules(basedir, importmode="prepend") snap1 = len(conftest._dirpath2confmods) assert snap1 == 1 - conftest._getconftestmodules(basedir.join("adir")) + conftest._getconftestmodules(basedir.join("adir"), importmode="prepend") assert len(conftest._dirpath2confmods) == snap1 + 1 - conftest._getconftestmodules(basedir.join("b")) + conftest._getconftestmodules(basedir.join("b"), importmode="prepend") assert len(conftest._dirpath2confmods) == snap1 + 2 def test_value_access_not_existing(self, basedir): conftest = ConftestWithSetinitial(basedir) with pytest.raises(KeyError): - conftest._rget_with_confmod("a", basedir) + conftest._rget_with_confmod("a", basedir, importmode="prepend") def test_value_access_by_path(self, basedir): conftest = ConftestWithSetinitial(basedir) adir = basedir.join("adir") - assert conftest._rget_with_confmod("a", adir)[1] == 1 - assert conftest._rget_with_confmod("a", adir.join("b"))[1] == 1.5 + assert conftest._rget_with_confmod("a", adir, importmode="prepend")[1] == 1 + assert ( + conftest._rget_with_confmod("a", adir.join("b"), importmode="prepend")[1] + == 1.5 + ) def test_value_access_with_confmod(self, basedir): startdir = basedir.join("adir", "b") startdir.ensure("xx", dir=True) conftest = ConftestWithSetinitial(startdir) - mod, value = conftest._rget_with_confmod("a", startdir) + mod, value = conftest._rget_with_confmod("a", startdir, importmode="prepend") assert value == 1.5 path = py.path.local(mod.__file__) assert path.dirpath() == basedir.join("adir", "b") @@ -91,7 +95,7 @@ def test_doubledash_considered(testdir): conf.ensure("conftest.py") conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.basename, conf.basename]) - values = conftest._getconftestmodules(conf) + values = conftest._getconftestmodules(conf, importmode="prepend") assert len(values) == 1 @@ -114,13 +118,13 @@ def test_conftest_global_import(testdir): import py, pytest from _pytest.config import PytestPluginManager conf = PytestPluginManager() - mod = conf._importconftest(py.path.local("conftest.py")) + mod = conf._importconftest(py.path.local("conftest.py"), importmode="prepend") assert mod.x == 3 import conftest assert conftest is mod, (conftest, mod) subconf = py.path.local().ensure("sub", "conftest.py") subconf.write("y=4") - mod2 = conf._importconftest(subconf) + mod2 = conf._importconftest(subconf, importmode="prepend") assert mod != mod2 assert mod2.y == 4 import conftest @@ -136,17 +140,17 @@ def test_conftestcutdir(testdir): p = testdir.mkdir("x") conftest = PytestPluginManager() conftest_setinitial(conftest, [testdir.tmpdir], confcutdir=p) - values = conftest._getconftestmodules(p) + values = conftest._getconftestmodules(p, importmode="prepend") assert len(values) == 0 - values = conftest._getconftestmodules(conf.dirpath()) + values = conftest._getconftestmodules(conf.dirpath(), importmode="prepend") assert len(values) == 0 assert conf not in conftest._conftestpath2mod # but we can still import a conftest directly - conftest._importconftest(conf) - values = conftest._getconftestmodules(conf.dirpath()) + conftest._importconftest(conf, importmode="prepend") + values = conftest._getconftestmodules(conf.dirpath(), importmode="prepend") assert values[0].__file__.startswith(str(conf)) # and all sub paths get updated properly - values = conftest._getconftestmodules(p) + values = conftest._getconftestmodules(p, importmode="prepend") assert len(values) == 1 assert values[0].__file__.startswith(str(conf)) @@ -155,7 +159,7 @@ def test_conftestcutdir_inplace_considered(testdir): conf = testdir.makeconftest("") conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath()) - values = conftest._getconftestmodules(conf.dirpath()) + values = conftest._getconftestmodules(conf.dirpath(), importmode="prepend") assert len(values) == 1 assert values[0].__file__.startswith(str(conf)) @@ -340,13 +344,13 @@ def test_conftest_import_order(testdir, monkeypatch): ct2 = sub.join("conftest.py") ct2.write("") - def impct(p): + def impct(p, importmode): return p conftest = PytestPluginManager() conftest._confcutdir = testdir.tmpdir monkeypatch.setattr(conftest, "_importconftest", impct) - assert conftest._getconftestmodules(sub) == [ct1, ct2] + assert conftest._getconftestmodules(sub, importmode="prepend") == [ct1, ct2] def test_fixture_dependency(testdir): diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index acc963199bc..126e1718e7a 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -1,6 +1,7 @@ import os.path import sys import unittest.mock +from textwrap import dedent import py @@ -9,11 +10,14 @@ from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import get_extended_length_path_str from _pytest.pathlib import get_lock_path +from _pytest.pathlib import import_path +from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import maybe_delete_a_numbered_dir from _pytest.pathlib import Path +from _pytest.pathlib import resolve_package_path -class TestPort: +class TestFNMatcherPort: """Test that our port of py.common.FNMatcher (fnmatch_ex) produces the same results as the original py.path.local.fnmatch method. """ @@ -79,6 +83,242 @@ def test_not_matching(self, match, pattern, path): assert not match(pattern, path) +class TestImportPath: + """ + + Most of the tests here were copied from py lib's tests for "py.local.path.pyimport". + + Having our own pyimport-like function is inline with removing py.path dependency in the future. + """ + + @pytest.yield_fixture(scope="session") + def path1(self, tmpdir_factory): + path = tmpdir_factory.mktemp("path") + self.setuptestfs(path) + yield path + assert path.join("samplefile").check() + + def setuptestfs(self, path): + # print "setting up test fs for", repr(path) + samplefile = path.ensure("samplefile") + samplefile.write("samplefile\n") + + execfile = path.ensure("execfile") + execfile.write("x=42") + + execfilepy = path.ensure("execfile.py") + execfilepy.write("x=42") + + d = {1: 2, "hello": "world", "answer": 42} + path.ensure("samplepickle").dump(d) + + sampledir = path.ensure("sampledir", dir=1) + sampledir.ensure("otherfile") + + otherdir = path.ensure("otherdir", dir=1) + otherdir.ensure("__init__.py") + + module_a = otherdir.ensure("a.py") + module_a.write("from .b import stuff as result\n") + module_b = otherdir.ensure("b.py") + module_b.write('stuff="got it"\n') + module_c = otherdir.ensure("c.py") + module_c.write( + dedent( + """ + import py; + import otherdir.a + value = otherdir.a.result + """ + ) + ) + module_d = otherdir.ensure("d.py") + module_d.write( + dedent( + """ + import py; + from otherdir import a + value2 = a.result + """ + ) + ) + + def test_smoke_test(self, path1): + obj = import_path(path1.join("execfile.py")) + assert obj.x == 42 # type: ignore[attr-defined] + assert obj.__name__ == "execfile" + + def test_renamed_dir_creates_mismatch(self, tmpdir, monkeypatch): + p = tmpdir.ensure("a", "test_x123.py") + import_path(p) + tmpdir.join("a").move(tmpdir.join("b")) + with pytest.raises(ImportPathMismatchError): + import_path(tmpdir.join("b", "test_x123.py")) + + # Errors can be ignored. + monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "1") + import_path(tmpdir.join("b", "test_x123.py")) + + # PY_IGNORE_IMPORTMISMATCH=0 does not ignore error. + monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "0") + with pytest.raises(ImportPathMismatchError): + import_path(tmpdir.join("b", "test_x123.py")) + + def test_messy_name(self, tmpdir): + # http://bitbucket.org/hpk42/py-trunk/issue/129 + path = tmpdir.ensure("foo__init__.py") + module = import_path(path) + assert module.__name__ == "foo__init__" + + def test_dir(self, tmpdir): + p = tmpdir.join("hello_123") + p_init = p.ensure("__init__.py") + m = import_path(p) + assert m.__name__ == "hello_123" + m = import_path(p_init) + assert m.__name__ == "hello_123" + + def test_a(self, path1): + otherdir = path1.join("otherdir") + mod = import_path(otherdir.join("a.py")) + assert mod.result == "got it" # type: ignore[attr-defined] + assert mod.__name__ == "otherdir.a" + + def test_b(self, path1): + otherdir = path1.join("otherdir") + mod = import_path(otherdir.join("b.py")) + assert mod.stuff == "got it" # type: ignore[attr-defined] + assert mod.__name__ == "otherdir.b" + + def test_c(self, path1): + otherdir = path1.join("otherdir") + mod = import_path(otherdir.join("c.py")) + assert mod.value == "got it" # type: ignore[attr-defined] + + def test_d(self, path1): + otherdir = path1.join("otherdir") + mod = import_path(otherdir.join("d.py")) + assert mod.value2 == "got it" # type: ignore[attr-defined] + + def test_import_after(self, tmpdir): + tmpdir.ensure("xxxpackage", "__init__.py") + mod1path = tmpdir.ensure("xxxpackage", "module1.py") + mod1 = import_path(mod1path) + assert mod1.__name__ == "xxxpackage.module1" + from xxxpackage import module1 + + assert module1 is mod1 + + def test_check_filepath_consistency(self, monkeypatch, tmpdir): + name = "pointsback123" + ModuleType = type(os) + p = tmpdir.ensure(name + ".py") + for ending in (".pyc", ".pyo"): + mod = ModuleType(name) + pseudopath = tmpdir.ensure(name + ending) + mod.__file__ = str(pseudopath) + monkeypatch.setitem(sys.modules, name, mod) + newmod = import_path(p) + assert mod == newmod + monkeypatch.undo() + mod = ModuleType(name) + pseudopath = tmpdir.ensure(name + "123.py") + mod.__file__ = str(pseudopath) + monkeypatch.setitem(sys.modules, name, mod) + with pytest.raises(ImportPathMismatchError) as excinfo: + import_path(p) + modname, modfile, orig = excinfo.value.args + assert modname == name + assert modfile == pseudopath + assert orig == p + assert issubclass(ImportPathMismatchError, ImportError) + + def test_issue131_on__init__(self, tmpdir): + # __init__.py files may be namespace packages, and thus the + # __file__ of an imported module may not be ourselves + # see issue + p1 = tmpdir.ensure("proja", "__init__.py") + p2 = tmpdir.ensure("sub", "proja", "__init__.py") + m1 = import_path(p1) + m2 = import_path(p2) + assert m1 == m2 + + def test_ensuresyspath_append(self, tmpdir): + root1 = tmpdir.mkdir("root1") + file1 = root1.ensure("x123.py") + assert str(root1) not in sys.path + import_path(file1, mode="append") + assert str(root1) == sys.path[-1] + assert str(root1) not in sys.path[:-1] + + def test_invalid_path(self, tmpdir): + with pytest.raises(ImportError): + import_path(tmpdir.join("invalid.py")) + + @pytest.fixture + def simple_module(self, tmpdir): + fn = tmpdir.join("mymod.py") + fn.write( + dedent( + """ + def foo(x): return 40 + x + """ + ) + ) + return fn + + def test_importmode_importlib(self, simple_module): + """importlib mode does not change sys.path""" + module = import_path(simple_module, mode="importlib") + assert module.foo(2) == 42 # type: ignore[attr-defined] + assert simple_module.dirname not in sys.path + + def test_importmode_twice_is_different_module(self, simple_module): + """importlib mode always returns a new module""" + module1 = import_path(simple_module, mode="importlib") + module2 = import_path(simple_module, mode="importlib") + assert module1 is not module2 + + def test_no_meta_path_found(self, simple_module, monkeypatch): + """Even without any meta_path should still import module""" + monkeypatch.setattr(sys, "meta_path", []) + module = import_path(simple_module, mode="importlib") + assert module.foo(2) == 42 # type: ignore[attr-defined] + + # mode='importlib' fails if no spec is found to load the module + import importlib.util + + monkeypatch.setattr( + importlib.util, "spec_from_file_location", lambda *args: None + ) + with pytest.raises(ImportError): + import_path(simple_module, mode="importlib") + + +def test_resolve_package_path(tmp_path): + pkg = tmp_path / "pkg1" + pkg.mkdir() + (pkg / "__init__.py").touch() + (pkg / "subdir").mkdir() + (pkg / "subdir/__init__.py").touch() + assert resolve_package_path(pkg) == pkg + assert resolve_package_path(pkg.joinpath("subdir", "__init__.py")) == pkg + + +def test_package_unimportable(tmp_path): + pkg = tmp_path / "pkg1-1" + pkg.mkdir() + pkg.joinpath("__init__.py").touch() + subdir = pkg.joinpath("subdir") + subdir.mkdir() + pkg.joinpath("subdir/__init__.py").touch() + assert resolve_package_path(subdir) == subdir + xyz = subdir.joinpath("xyz.py") + xyz.touch() + assert resolve_package_path(xyz) == subdir + assert not resolve_package_path(pkg) + + def test_access_denied_during_cleanup(tmp_path, monkeypatch): """Ensure that deleting a numbered dir does not fail because of OSErrors (#4262).""" path = tmp_path / "temp-1" diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 713687578f4..fc327e6c058 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -37,7 +37,7 @@ def pytest_myhook(xyz): pm.hook.pytest_addhooks.call_historic( kwargs=dict(pluginmanager=config.pluginmanager) ) - config.pluginmanager._importconftest(conf) + config.pluginmanager._importconftest(conf, importmode="prepend") # print(config.pluginmanager.get_plugins()) res = config.hook.pytest_myhook(xyz=10) assert res == [11] @@ -64,7 +64,7 @@ def pytest_addoption(parser): default=True) """ ) - config.pluginmanager._importconftest(p) + config.pluginmanager._importconftest(p, importmode="prepend") assert config.option.test123 def test_configure(self, testdir): @@ -129,10 +129,10 @@ def test_hook_proxy(self, testdir): conftest1 = testdir.tmpdir.join("tests/conftest.py") conftest2 = testdir.tmpdir.join("tests/subdir/conftest.py") - config.pluginmanager._importconftest(conftest1) + config.pluginmanager._importconftest(conftest1, importmode="prepend") ihook_a = session.gethookproxy(testdir.tmpdir.join("tests")) assert ihook_a is not None - config.pluginmanager._importconftest(conftest2) + config.pluginmanager._importconftest(conftest2, importmode="prepend") ihook_b = session.gethookproxy(testdir.tmpdir.join("tests")) assert ihook_a is not ihook_b From 320625527a1372395f1117e635278050532cbbfe Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 13 Jun 2020 11:22:18 -0400 Subject: [PATCH 377/823] Add more tests and docs --- changelog/7346.feature.rst | 1 + doc/en/reference.rst | 4 +++- src/_pytest/config/__init__.py | 2 +- testing/test_config.py | 16 ++++++++-------- 4 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 changelog/7346.feature.rst diff --git a/changelog/7346.feature.rst b/changelog/7346.feature.rst new file mode 100644 index 00000000000..fef0bbb781c --- /dev/null +++ b/changelog/7346.feature.rst @@ -0,0 +1 @@ +Version information as defined by `PEP 440 `_ may now be included when providing plugins to the ``required_plugins`` configuration option. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index bf3d1fbbbb6..d5580ad6595 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1562,12 +1562,14 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: required_plugins A space separated list of plugins that must be present for pytest to run. + Plugins can be listed with or without version specifiers directly following + their name. Whitespace between different version specifiers is not allowed. If any one of the plugins is not found, emit an error. .. code-block:: ini [pytest] - required_plugins = pytest-html pytest-xdist + required_plugins = pytest-django>=3.0.0,<4.0.0 pytest-html pytest-xdist>=1.0.0 .. confval:: testpaths diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index a124b8f5f1f..116383a7386 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1101,7 +1101,7 @@ def _validate_plugins(self) -> None: plugin_info = self.pluginmanager.list_plugin_distinfo() plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info} - missing_plugins = ["a"] + missing_plugins = [] for required_plugin in required_plugins: spec = None try: diff --git a/testing/test_config.py b/testing/test_config.py index f8c1c879ead..d59d641f6ab 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -265,31 +265,31 @@ def test_invalid_ini_keys( ( """ [pytest] - required_plugins = pytest-xdist~=1.32.0 pytest-xdist==1.32.0 pytest-xdist!=0.0.1 pytest-xdist<=99.99.0 - pytest-xdist>=1.32.0 pytest-xdist<9.9.9 pytest-xdist>1.30.0 pytest-xdist===1.32.0 + required_plugins = pytest-xdist>1.0.0,<2.0.0 """, "", ), ( """ [pytest] - required_plugins = pytest-xdist>9.9.9 pytest-xdist==1.32.0 pytest-xdist==8.8.8 + required_plugins = pytest-xdist~=1.32.0 pytest-xdist==1.32.0 pytest-xdist!=0.0.1 pytest-xdist<=99.99.0 + pytest-xdist>=1.32.0 pytest-xdist<9.9.9 pytest-xdist>1.30.0 pytest-xdist===1.32.0 """, - "Missing required plugins: pytest-xdist==8.8.8, pytest-xdist>9.9.9", + "", ), ( """ [pytest] - required_plugins = pytest-xdist==aegsrgrsgs + required_plugins = pytest-xdist>9.9.9 pytest-xdist==1.32.0 pytest-xdist==8.8.8 """, - "Missing required plugins: pytest-xdist==aegsrgrsgs", + "Missing required plugins: pytest-xdist==8.8.8, pytest-xdist>9.9.9", ), ( """ [pytest] - required_plugins = pytest-xdist==-1 + required_plugins = pytest-xdist==aegsrgrsgs pytest-xdist==-1 pytest-xdist>2.1.1,>3.0.0 """, - "Missing required plugins: pytest-xdist==-1", + "Missing required plugins: pytest-xdist==-1, pytest-xdist==aegsrgrsgs, pytest-xdist>2.1.1,>3.0.0", ), ( """ From 03d0a10e3a52a089dc4614757a11a931e29b2eaf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 13 Jun 2020 20:50:24 +0300 Subject: [PATCH 378/823] The final Python 2.7 was released in April The final Python 2.7.18 release was on 20 Apr 2020. https://mail.python.org/archives/list/python-dev@python.org/thread/OFCIETIXLX34X7FVK5B5WPZH22HXV342/#OFCIETIXLX34X7FVK5B5WPZH22HXV342 --- doc/en/py27-py34-deprecation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/py27-py34-deprecation.rst b/doc/en/py27-py34-deprecation.rst index f2d6b540dbc..f23f2062b79 100644 --- a/doc/en/py27-py34-deprecation.rst +++ b/doc/en/py27-py34-deprecation.rst @@ -9,7 +9,7 @@ In case of Python 2 and 3, the difference between the languages makes it even mo because many new Python 3 features cannot be used in a Python 2/3 compatible code base. Python 2.7 EOL has been reached `in 2020 `__, with -the last release planned for mid-April, 2020. +the last release made in April, 2020. Python 3.4 EOL has been reached `in 2019 `__, with the last release made in March, 2019. From 25064eba7a742bc1bbcef370e2e0f44f40bc0bde Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 13 Jun 2020 14:15:54 +0300 Subject: [PATCH 379/823] pytest.collect: type annotate (backward compat module) This is just to satisfy typing coverage. --- src/pytest/collect.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pytest/collect.py b/src/pytest/collect.py index 73c9d35a0df..ec9c2d8b4e1 100644 --- a/src/pytest/collect.py +++ b/src/pytest/collect.py @@ -1,6 +1,8 @@ import sys import warnings from types import ModuleType +from typing import Any +from typing import List import pytest from _pytest.deprecated import PYTEST_COLLECT_MODULE @@ -20,15 +22,15 @@ class FakeCollectModule(ModuleType): - def __init__(self): + def __init__(self) -> None: super().__init__("pytest.collect") self.__all__ = list(COLLECT_FAKEMODULE_ATTRIBUTES) self.__pytest = pytest - def __dir__(self): + def __dir__(self) -> List[str]: return dir(super()) + self.__all__ - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: if name not in self.__all__: raise AttributeError(name) warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2) From bb7b3af9b95bafe633a2a5136e78dcf1d686c4de Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 13 Jun 2020 22:28:55 +0300 Subject: [PATCH 380/823] hookspec: fix return type annotation of pytest_runtest_makereport --- src/_pytest/hookspec.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 1c1726d53b0..4469f507826 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -437,7 +437,9 @@ def pytest_runtest_teardown(item: "Item", nextitem: "Optional[Item]") -> None: @hookspec(firstresult=True) -def pytest_runtest_makereport(item: "Item", call: "CallInfo[None]") -> Optional[object]: +def pytest_runtest_makereport( + item: "Item", call: "CallInfo[None]" +) -> Optional["TestReport"]: """ return a :py:class:`_pytest.runner.TestReport` object for the given :py:class:`pytest.Item <_pytest.main.Item>` and :py:class:`_pytest.runner.CallInfo`. From 314d00968a0e5d3532fde41297ffa884b6d548bb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jun 2020 12:49:05 +0300 Subject: [PATCH 381/823] hookspec: type annotate pytest_runtest_log{start,finish} --- src/_pytest/hookspec.py | 8 ++++++-- src/_pytest/logging.py | 4 ++-- src/_pytest/terminal.py | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 4469f507826..a893517aa9a 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -396,7 +396,9 @@ def pytest_runtest_protocol( Stops at first non-None result, see :ref:`firstresult` """ -def pytest_runtest_logstart(nodeid, location): +def pytest_runtest_logstart( + nodeid: str, location: Tuple[str, Optional[int], str] +) -> None: """ signal the start of running a single test item. This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and @@ -407,7 +409,9 @@ def pytest_runtest_logstart(nodeid, location): """ -def pytest_runtest_logfinish(nodeid, location): +def pytest_runtest_logfinish( + nodeid: str, location: Tuple[str, Optional[int], str] +) -> None: """ signal the complete finish of running a single test item. This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 04bf74b6c57..8755e5611a3 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -653,12 +653,12 @@ def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]: yield # run all the tests @pytest.hookimpl - def pytest_runtest_logstart(self): + def pytest_runtest_logstart(self) -> None: self.log_cli_handler.reset() self.log_cli_handler.set_when("start") @pytest.hookimpl - def pytest_runtest_logreport(self): + def pytest_runtest_logreport(self) -> None: self.log_cli_handler.set_when("logreport") def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index f98c9b8abb4..6a58260e99f 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -502,7 +502,9 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: def pytest_deselected(self, items) -> None: self._add_stats("deselected", items) - def pytest_runtest_logstart(self, nodeid, location) -> None: + def pytest_runtest_logstart( + self, nodeid: str, location: Tuple[str, Optional[int], str] + ) -> None: # ensure that the path is printed before the # 1st test of a module starts running if self.showlongtestinfo: @@ -569,7 +571,7 @@ def _is_last_item(self) -> bool: assert self._session is not None return len(self._progress_nodeids_reported) == self._session.testscollected - def pytest_runtest_logfinish(self, nodeid) -> None: + def pytest_runtest_logfinish(self, nodeid: str) -> None: assert self._session if self.verbosity <= 0 and self._show_progress_info: if self._show_progress_info == "count": From bb878a2b13508e88ace7718759065de56a63aac5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jun 2020 13:35:59 +0300 Subject: [PATCH 382/823] runner: don't try to teardown previous items from pytest_runtest_setup While working on improving the documentation of the `pytest_runtest_setup` hook, I came up with this text: > Called to perform the setup phase of the test item. > > The default implementation runs ``setup()`` on item and all of its > parents (which haven't been setup yet). This includes obtaining the > values of fixtures required by the item (which haven't been obtained > yet). But upon closer inspection I noticed this line at the start of `SetupState.prepare` (which is what does the actual work for `pytest_runtest_setup`): self._teardown_towards(needed_collectors) which implies that the setup phase of one item might trigger teardowns of *previous* items. This complicates the simple explanation. It also seems like a completely undesirable thing to do, because it breaks isolation between tests -- e.g. a failed teardown of one item shouldn't cause the failure of some other items just because it happens to run after it. So the first thing I tried was to remove that line and see if anything breaks -- nothing did. At least pytest's own test suite runs fine. So maybe it's just dead code? --- src/_pytest/runner.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 3ca8d7ea46d..b6c89dce5f5 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -409,16 +409,15 @@ def _teardown_towards(self, needed_collectors) -> None: raise exc def prepare(self, colitem) -> None: - """ setup objects along the collector chain to the test-method - and teardown previously setup objects.""" - needed_collectors = colitem.listchain() - self._teardown_towards(needed_collectors) + """Setup objects along the collector chain to the test-method.""" # check if the last collection node has raised an error for col in self.stack: if hasattr(col, "_prepare_exc"): exc = col._prepare_exc # type: ignore[attr-defined] # noqa: F821 raise exc + + needed_collectors = colitem.listchain() for col in needed_collectors[len(self.stack) :]: self.stack.append(col) try: From 5e35c86a376e48453749ebc1997fb60b6262ce19 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jun 2020 16:52:09 +0300 Subject: [PATCH 383/823] doc/reference: refer to function public names instead of internal _pytest names This way e.g. a :py:func:`pytest.exit` cross-reference works properly. --- doc/en/changelog.rst | 4 ++-- doc/en/reference.rst | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 2806fb6a3e5..40b5ee69014 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -287,7 +287,7 @@ Bug Fixes - `#6646 `_: Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's :func:`testdir.runpytest <_pytest.pytester.Testdir.runpytest>` etc. -- `#6660 `_: :func:`pytest.exit() <_pytest.outcomes.exit>` is handled when emitted from the :func:`pytest_sessionfinish <_pytest.hookspec.pytest_sessionfinish>` hook. This includes quitting from a debugger. +- `#6660 `_: :py:func:`pytest.exit` is handled when emitted from the :func:`pytest_sessionfinish <_pytest.hookspec.pytest_sessionfinish>` hook. This includes quitting from a debugger. - `#6752 `_: When :py:func:`pytest.raises` is used as a function (as opposed to a context manager), @@ -399,7 +399,7 @@ Improvements - `#6231 `_: Improve check for misspelling of :ref:`pytest.mark.parametrize ref`. -- `#6257 `_: Handle :py:func:`_pytest.outcomes.exit` being used via :py:func:`~_pytest.hookspec.pytest_internalerror`, e.g. when quitting pdb from post mortem. +- `#6257 `_: Handle :py:func:`pytest.exit` being used via :py:func:`~_pytest.hookspec.pytest_internalerror`, e.g. when quitting pdb from post mortem. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index bf3d1fbbbb6..d21cdd3e95d 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -15,41 +15,41 @@ Functions pytest.approx ~~~~~~~~~~~~~ -.. autofunction:: _pytest.python_api.approx +.. autofunction:: pytest.approx pytest.fail ~~~~~~~~~~~ **Tutorial**: :ref:`skipping` -.. autofunction:: _pytest.outcomes.fail +.. autofunction:: pytest.fail pytest.skip ~~~~~~~~~~~ -.. autofunction:: _pytest.outcomes.skip(msg, [allow_module_level=False]) +.. autofunction:: pytest.skip(msg, [allow_module_level=False]) .. _`pytest.importorskip ref`: pytest.importorskip ~~~~~~~~~~~~~~~~~~~ -.. autofunction:: _pytest.outcomes.importorskip +.. autofunction:: pytest.importorskip pytest.xfail ~~~~~~~~~~~~ -.. autofunction:: _pytest.outcomes.xfail +.. autofunction:: pytest.xfail pytest.exit ~~~~~~~~~~~ -.. autofunction:: _pytest.outcomes.exit +.. autofunction:: pytest.exit pytest.main ~~~~~~~~~~~ -.. autofunction:: _pytest.config.main +.. autofunction:: pytest.main pytest.param ~~~~~~~~~~~~ From 2a38ca8a0cc9f59266874ca3cf3c0c8b080fd42e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jun 2020 16:02:25 +0300 Subject: [PATCH 384/823] doc/reference: add CollectReport CollectReport appears in several hooks, so we should document it. It's runtest equivalent TestReport is already documented. --- doc/en/reference.rst | 8 ++++++++ src/_pytest/reports.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index d21cdd3e95d..bc501c1ad4f 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -762,6 +762,14 @@ Collector :members: :show-inheritance: +CollectReport +~~~~~~~~~~~~~ + +.. autoclass:: _pytest.runner.CollectReport() + :members: + :show-inheritance: + :inherited-members: + Config ~~~~~~ diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 6a408354b03..8b213ed1342 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -335,6 +335,8 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": class CollectReport(BaseReport): + """Collection report object.""" + when = "collect" def __init__( @@ -346,11 +348,24 @@ def __init__( sections: Iterable[Tuple[str, str]] = (), **extra ) -> None: + #: normalized collection node id self.nodeid = nodeid + + #: test outcome, always one of "passed", "failed", "skipped". self.outcome = outcome + + #: None or a failure representation. self.longrepr = longrepr + + #: The collected items and collection nodes. self.result = result or [] + + #: list of pairs ``(str, str)`` of extra information which needs to + #: marshallable. Used by pytest to add captured text + #: from ``stdout`` and ``stderr``, but may be used by other plugins + #: to add arbitrary information to reports. self.sections = list(sections) + self.__dict__.update(extra) @property From da1124eb9834a6a6839eeb05b6adc75513735598 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jun 2020 12:42:09 +0300 Subject: [PATCH 385/823] hookspec: improve runtest hooks documentation --- doc/en/reference.rst | 11 ++-- src/_pytest/hookspec.py | 128 ++++++++++++++++++++++++++++------------ 2 files changed, 95 insertions(+), 44 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index bc501c1ad4f..d09aecafb63 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -644,8 +644,8 @@ Initialization hooks called for plugins and ``conftest.py`` files. .. autofunction:: pytest_plugin_registered -Test running hooks -~~~~~~~~~~~~~~~~~~ +Test running (runtest) hooks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ All runtest related hooks receive a :py:class:`pytest.Item <_pytest.main.Item>` object. @@ -664,9 +664,6 @@ in :py:mod:`_pytest.pdb` which interacts with :py:mod:`_pytest.capture` and its input/output capturing in order to immediately drop into interactive debugging when a test failure occurs. -The :py:mod:`_pytest.terminal` reported specifically uses -the reporting hook to print information about a test run. - .. autofunction:: pytest_pyfunc_call Collection hooks @@ -765,7 +762,7 @@ Collector CollectReport ~~~~~~~~~~~~~ -.. autoclass:: _pytest.runner.CollectReport() +.. autoclass:: _pytest.reports.CollectReport() :members: :show-inheritance: :inherited-members: @@ -889,7 +886,7 @@ Session TestReport ~~~~~~~~~~ -.. autoclass:: _pytest.runner.TestReport() +.. autoclass:: _pytest.reports.TestReport() :members: :show-inheritance: :inherited-members: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index a893517aa9a..eba6f5ba9f2 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -361,18 +361,28 @@ def pytest_make_parametrize_id( # ------------------------------------------------------------------------- -# generic runtest related hooks +# runtest related hooks # ------------------------------------------------------------------------- @hookspec(firstresult=True) def pytest_runtestloop(session: "Session") -> Optional[object]: - """ called for performing the main runtest loop - (after collection finished). + """Performs the main runtest loop (after collection finished). - Stops at first non-None result, see :ref:`firstresult` + The default hook implementation performs the runtest protocol for all items + collected in the session (``session.items``), unless the collection failed + or the ``collectonly`` pytest option is set. - :param _pytest.main.Session session: the pytest session object + If at any point :py:func:`pytest.exit` is called, the loop is + terminated immediately. + + If at any point ``session.shouldfail`` or ``session.shouldstop`` are set, the + loop is terminated after the runtest protocol for the current item is finished. + + :param _pytest.main.Session session: The pytest session object. + + Stops at first non-None result, see :ref:`firstresult`. + The return value is not used, but only stops further processing. """ @@ -380,60 +390,91 @@ def pytest_runtestloop(session: "Session") -> Optional[object]: def pytest_runtest_protocol( item: "Item", nextitem: "Optional[Item]" ) -> Optional[object]: - """ implements the runtest_setup/call/teardown protocol for - the given test item, including capturing exceptions and calling - reporting hooks. + """Performs the runtest protocol for a single test item. - :arg item: test item for which the runtest protocol is performed. + The default runtest protocol is this (see individual hooks for full details): - :arg nextitem: the scheduled-to-be-next test item (or None if this - is the end my friend). This argument is passed on to - :py:func:`pytest_runtest_teardown`. + - ``pytest_runtest_logstart(nodeid, location)`` - :return boolean: True if no further hook implementations should be invoked. + - Setup phase: + - ``call = pytest_runtest_setup(item)`` (wrapped in ``CallInfo(when="setup")``) + - ``report = pytest_runtest_makereport(item, call)`` + - ``pytest_runtest_logreport(report)`` + - ``pytest_exception_interact(call, report)`` if an interactive exception occurred + - Call phase, if the the setup passed and the ``setuponly`` pytest option is not set: + - ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``) + - ``report = pytest_runtest_makereport(item, call)`` + - ``pytest_runtest_logreport(report)`` + - ``pytest_exception_interact(call, report)`` if an interactive exception occurred - Stops at first non-None result, see :ref:`firstresult` """ + - Teardown phase: + - ``call = pytest_runtest_teardown(item, nextitem)`` (wrapped in ``CallInfo(when="teardown")``) + - ``report = pytest_runtest_makereport(item, call)`` + - ``pytest_runtest_logreport(report)`` + - ``pytest_exception_interact(call, report)`` if an interactive exception occurred + + - ``pytest_runtest_logfinish(nodeid, location)`` + + :arg item: Test item for which the runtest protocol is performed. + + :arg nextitem: The scheduled-to-be-next test item (or None if this is the end my friend). + + Stops at first non-None result, see :ref:`firstresult`. + The return value is not used, but only stops further processing. + """ def pytest_runtest_logstart( nodeid: str, location: Tuple[str, Optional[int], str] ) -> None: - """ signal the start of running a single test item. + """Called at the start of running the runtest protocol for a single item. - This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and - :func:`pytest_runtest_teardown` hooks. + See :func:`pytest_runtest_protocol` for a description of the runtest protocol. - :param str nodeid: full id of the item - :param location: a triple of ``(filename, linenum, testname)`` + :param str nodeid: Full node ID of the item. + :param location: A triple of ``(filename, lineno, testname)``. """ def pytest_runtest_logfinish( nodeid: str, location: Tuple[str, Optional[int], str] ) -> None: - """ signal the complete finish of running a single test item. + """Called at the end of running the runtest protocol for a single item. - This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and - :func:`pytest_runtest_teardown` hooks. + See :func:`pytest_runtest_protocol` for a description of the runtest protocol. - :param str nodeid: full id of the item - :param location: a triple of ``(filename, linenum, testname)`` + :param str nodeid: Full node ID of the item. + :param location: A triple of ``(filename, lineno, testname)``. """ def pytest_runtest_setup(item: "Item") -> None: - """ called before ``pytest_runtest_call(item)``. """ + """Called to perform the setup phase for a test item. + + The default implementation runs ``setup()`` on ``item`` and all of its + parents (which haven't been setup yet). This includes obtaining the + values of fixtures required by the item (which haven't been obtained + yet). + """ def pytest_runtest_call(item: "Item") -> None: - """ called to execute the test ``item``. """ + """Called to run the test for test item (the call phase). + + The default implementation calls ``item.runtest()``. + """ def pytest_runtest_teardown(item: "Item", nextitem: "Optional[Item]") -> None: - """ called after ``pytest_runtest_call``. + """Called to perform the teardown phase for a test item. + + The default implementation runs the finalizers and calls ``teardown()`` + on ``item`` and all of its parents (which need to be torn down). This + includes running the teardown phase of fixtures required by the item (if + they go out of scope). - :arg nextitem: the scheduled-to-be-next test item (None if no further + :arg nextitem: The scheduled-to-be-next test item (None if no further test item is scheduled). This argument can be used to perform exact teardowns, i.e. calling just enough finalizers so that nextitem only needs to call setup-functions. @@ -444,16 +485,23 @@ def pytest_runtest_teardown(item: "Item", nextitem: "Optional[Item]") -> None: def pytest_runtest_makereport( item: "Item", call: "CallInfo[None]" ) -> Optional["TestReport"]: - """ return a :py:class:`_pytest.runner.TestReport` object - for the given :py:class:`pytest.Item <_pytest.main.Item>` and - :py:class:`_pytest.runner.CallInfo`. + """Called to create a :py:class:`_pytest.reports.TestReport` for each of + the setup, call and teardown runtest phases of a test item. - Stops at first non-None result, see :ref:`firstresult` """ + See :func:`pytest_runtest_protocol` for a description of the runtest protocol. + + :param CallInfo[None] call: The ``CallInfo`` for the phase. + + Stops at first non-None result, see :ref:`firstresult`. + """ def pytest_runtest_logreport(report: "TestReport") -> None: - """ process a test setup/call/teardown report relating to - the respective phase of executing a test. """ + """Process the :py:class:`_pytest.reports.TestReport` produced for each + of the setup, call and teardown runtest phases of an item. + + See :func:`pytest_runtest_protocol` for a description of the runtest protocol. + """ @hookspec(firstresult=True) @@ -785,11 +833,17 @@ def pytest_keyboard_interrupt( def pytest_exception_interact( node: "Node", call: "CallInfo[object]", report: "Union[CollectReport, TestReport]" ) -> None: - """called when an exception was raised which can potentially be + """Called when an exception was raised which can potentially be interactively handled. - This hook is only called if an exception was raised - that is not an internal exception like ``skip.Exception``. + May be called during collection (see :py:func:`pytest_make_collect_report`), + in which case ``report`` is a :py:class:`_pytest.reports.CollectReport`. + + May be called during runtest of an item (see :py:func:`pytest_runtest_protocol`), + in which case ``report`` is a :py:class:`_pytest.reports.TestReport`. + + This hook is not called if the exception that was raised is an internal + exception like ``skip.Exception``. """ From 33804fd9b79c27aac4acd71617694fe1ef0fd01b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jun 2020 16:31:55 +0300 Subject: [PATCH 386/823] doc/reference: move "Collection hooks" before "Test running hooks" Collection occurs before test running, so it seems more logical. --- doc/en/reference.rst | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index d09aecafb63..11788ee6c68 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -644,28 +644,6 @@ Initialization hooks called for plugins and ``conftest.py`` files. .. autofunction:: pytest_plugin_registered -Test running (runtest) hooks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -All runtest related hooks receive a :py:class:`pytest.Item <_pytest.main.Item>` object. - -.. autofunction:: pytest_runtestloop -.. autofunction:: pytest_runtest_protocol -.. autofunction:: pytest_runtest_logstart -.. autofunction:: pytest_runtest_logfinish -.. autofunction:: pytest_runtest_setup -.. autofunction:: pytest_runtest_call -.. autofunction:: pytest_runtest_teardown -.. autofunction:: pytest_runtest_makereport - -For deeper understanding you may look at the default implementation of -these hooks in :py:mod:`_pytest.runner` and maybe also -in :py:mod:`_pytest.pdb` which interacts with :py:mod:`_pytest.capture` -and its input/output capturing in order to immediately drop -into interactive debugging when a test failure occurs. - -.. autofunction:: pytest_pyfunc_call - Collection hooks ~~~~~~~~~~~~~~~~ @@ -691,6 +669,28 @@ items, delete or otherwise amend the test items: .. autofunction:: pytest_collection_finish +Test running (runtest) hooks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All runtest related hooks receive a :py:class:`pytest.Item <_pytest.main.Item>` object. + +.. autofunction:: pytest_runtestloop +.. autofunction:: pytest_runtest_protocol +.. autofunction:: pytest_runtest_logstart +.. autofunction:: pytest_runtest_logfinish +.. autofunction:: pytest_runtest_setup +.. autofunction:: pytest_runtest_call +.. autofunction:: pytest_runtest_teardown +.. autofunction:: pytest_runtest_makereport + +For deeper understanding you may look at the default implementation of +these hooks in :py:mod:`_pytest.runner` and maybe also +in :py:mod:`_pytest.pdb` which interacts with :py:mod:`_pytest.capture` +and its input/output capturing in order to immediately drop +into interactive debugging when a test failure occurs. + +.. autofunction:: pytest_pyfunc_call + Reporting hooks ~~~~~~~~~~~~~~~ From c27550731d01beb81b8841213dd2f107a82bd6e0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 15 Jun 2020 17:14:48 +0300 Subject: [PATCH 387/823] Require py>=1.8.2 so we can rely on correct hash() of py.path.local on Windows See https://github.com/pytest-dev/py/blob/1.8.2/CHANGELOG#L4. Fixes #7357. --- changelog/7357.trivial.rst | 1 + setup.cfg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/7357.trivial.rst diff --git a/changelog/7357.trivial.rst b/changelog/7357.trivial.rst new file mode 100644 index 00000000000..f0f9d035dfc --- /dev/null +++ b/changelog/7357.trivial.rst @@ -0,0 +1 @@ +py>=1.8.2 is now required. diff --git a/setup.cfg b/setup.cfg index 8749334f88c..3e5cfa1f869 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ install_requires = more-itertools>=4.0.0 packaging pluggy>=0.12,<1.0 - py>=1.5.0 + py>=1.8.2 toml atomicwrites>=1.0;sys_platform=="win32" colorama;sys_platform=="win32" From a67c553beb0253f30262b4e44a1a929f316aebc7 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 16 Jun 2020 05:39:36 -0400 Subject: [PATCH 388/823] Disable caching when evaluating expressions in marks (#7373) --- changelog/7360.bugfix.rst | 2 ++ src/_pytest/mark/evaluate.py | 21 +++++---------------- testing/test_mark.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 changelog/7360.bugfix.rst diff --git a/changelog/7360.bugfix.rst b/changelog/7360.bugfix.rst new file mode 100644 index 00000000000..b84ce461473 --- /dev/null +++ b/changelog/7360.bugfix.rst @@ -0,0 +1,2 @@ +Fix possibly incorrect evaluation of string expressions passed to ``pytest.mark.skipif`` and ``pytest.mark.xfail``, +in rare circumstances where the exact same string is used but refers to different global values. diff --git a/src/_pytest/mark/evaluate.py b/src/_pytest/mark/evaluate.py index 759191668aa..eb9903a5976 100644 --- a/src/_pytest/mark/evaluate.py +++ b/src/_pytest/mark/evaluate.py @@ -10,25 +10,14 @@ from ..outcomes import fail from ..outcomes import TEST_OUTCOME from .structures import Mark -from _pytest.config import Config from _pytest.nodes import Item -from _pytest.store import StoreKey -evalcache_key = StoreKey[Dict[str, Any]]() +def compiled_eval(expr: str, d: Dict[str, object]) -> Any: + import _pytest._code - -def cached_eval(config: Config, expr: str, d: Dict[str, object]) -> Any: - default = {} # type: Dict[str, object] - evalcache = config._store.setdefault(evalcache_key, default) - try: - return evalcache[expr] - except KeyError: - import _pytest._code - - exprcode = _pytest._code.compile(expr, mode="eval") - evalcache[expr] = x = eval(exprcode, d) - return x + exprcode = _pytest._code.compile(expr, mode="eval") + return eval(exprcode, d) class MarkEvaluator: @@ -98,7 +87,7 @@ def _istrue(self) -> bool: self.expr = expr if isinstance(expr, str): d = self._getglobals() - result = cached_eval(self.item.config, expr, d) + result = compiled_eval(expr, d) else: if "reason" not in mark.kwargs: # XXX better be checked at collection time diff --git a/testing/test_mark.py b/testing/test_mark.py index cdd4df9ddaa..f261c8922ad 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -706,6 +706,36 @@ def test_1(parameter): reprec = testdir.inline_run() reprec.assertoutcome(skipped=1) + def test_reevaluate_dynamic_expr(self, testdir): + """#7360""" + py_file1 = testdir.makepyfile( + test_reevaluate_dynamic_expr1=""" + import pytest + + skip = True + + @pytest.mark.skipif("skip") + def test_should_skip(): + assert True + """ + ) + py_file2 = testdir.makepyfile( + test_reevaluate_dynamic_expr2=""" + import pytest + + skip = False + + @pytest.mark.skipif("skip") + def test_should_not_skip(): + assert True + """ + ) + + file_name1 = os.path.basename(py_file1.strpath) + file_name2 = os.path.basename(py_file2.strpath) + reprec = testdir.inline_run(file_name1, file_name2) + reprec.assertoutcome(passed=1, skipped=1) + class TestKeywordSelection: def test_select_simple(self, testdir): From ab19148c2a3d915fab6231a9f9053d771bc63eaf Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Tue, 16 Jun 2020 20:59:58 -0400 Subject: [PATCH 389/823] fix changelog file name for issue 4049 fix --- changelog/{7255.feature.rst => 4049.feature.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{7255.feature.rst => 4049.feature.rst} (100%) diff --git a/changelog/7255.feature.rst b/changelog/4049.feature.rst similarity index 100% rename from changelog/7255.feature.rst rename to changelog/4049.feature.rst From 4cc4ebf3c9f79eb9d7484c763b11dea3ddff4b82 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Thu, 18 Jun 2020 11:58:41 -0400 Subject: [PATCH 390/823] Don't treat ini keys defined in conftest.py as invalid (#7384) --- src/_pytest/config/__init__.py | 2 +- testing/test_config.py | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4ff6ce70713..31b73a2c927 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1054,7 +1054,6 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: args, namespace=copy.copy(self.option) ) self._validate_plugins() - self._validate_keys() if self.known_args_namespace.confcutdir is None and self.inifile: confcutdir = py.path.local(self.inifile).dirname self.known_args_namespace.confcutdir = confcutdir @@ -1077,6 +1076,7 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: ) else: raise + self._validate_keys() def _checkversion(self): import pytest diff --git a/testing/test_config.py b/testing/test_config.py index d59d641f6ab..c9eea7a1675 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -203,6 +203,15 @@ def test_confcutdir(self, testdir): """ [pytest] minversion = 5.0.0 + """, + [], + [], + "", + ), + ( + """ + [pytest] + conftest_ini_key = 1 """, [], [], @@ -213,16 +222,25 @@ def test_confcutdir(self, testdir): def test_invalid_ini_keys( self, testdir, ini_file_text, invalid_keys, stderr_output, exception_text ): + testdir.makeconftest( + """ + def pytest_addoption(parser): + parser.addini("conftest_ini_key", "") + """ + ) testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) + config = testdir.parseconfig() assert sorted(config._get_unknown_ini_keys()) == sorted(invalid_keys) result = testdir.runpytest() result.stderr.fnmatch_lines(stderr_output) - if stderr_output: + if exception_text: with pytest.raises(pytest.fail.Exception, match=exception_text): testdir.runpytest("--strict-config") + else: + testdir.runpytest("--strict-config") @pytest.mark.parametrize( "ini_file_text, exception_text", From a1f841d5d26364b45de70f5a61a03adecc6b5462 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 19 Jun 2020 13:33:53 +0300 Subject: [PATCH 391/823] skipping: use pytest_runtest_call instead of pytest_pyfunc_call `@pytest.mark.xfail` is meant to work with arbitrary items, and there is a test `test_mark_xfail_item` which verifies this. However, the code for some reason uses `pytest_pyfunc_call` for the call phase check, which only works for Function items. The test mentioned above only passed "accidentally" because the `pytest_runtest_makereport` hook also runs a `evalxfail.istrue()` which triggers and evaluation, but conceptually it shouldn't do that. Change to `pytest_runtest_call` to make the xfail checking properly generic. --- src/_pytest/skipping.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index bbd4593fd40..4e4b5a3c476 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -10,7 +10,6 @@ from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.outcomes import xfail -from _pytest.python import Function from _pytest.reports import BaseReport from _pytest.runner import CallInfo from _pytest.store import StoreKey @@ -103,12 +102,12 @@ def pytest_runtest_setup(item: Item) -> None: @hookimpl(hookwrapper=True) -def pytest_pyfunc_call(pyfuncitem: Function): - check_xfail_no_run(pyfuncitem) +def pytest_runtest_call(item: Item): + check_xfail_no_run(item) outcome = yield passed = outcome.excinfo is None if passed: - check_strict_xfail(pyfuncitem) + check_strict_xfail(item) def check_xfail_no_run(item: Item) -> None: @@ -120,14 +119,14 @@ def check_xfail_no_run(item: Item) -> None: xfail("[NOTRUN] " + evalxfail.getexplanation()) -def check_strict_xfail(pyfuncitem: Function) -> None: +def check_strict_xfail(item: Item) -> None: """check xfail(strict=True) for the given PASSING test""" - evalxfail = pyfuncitem._store[evalxfail_key] + evalxfail = item._store[evalxfail_key] if evalxfail.istrue(): - strict_default = pyfuncitem.config.getini("xfail_strict") + strict_default = item.config.getini("xfail_strict") is_strict_xfail = evalxfail.get("strict", strict_default) if is_strict_xfail: - del pyfuncitem._store[evalxfail_key] + del item._store[evalxfail_key] explanation = evalxfail.getexplanation() fail("[XPASS(strict)] " + explanation, pytrace=False) From 6072c9950d76a20da5397547932557842b84e078 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 19 Jun 2020 13:33:54 +0300 Subject: [PATCH 392/823] skipping: move MarkEvaluator from _pytest.mark.evaluate to _pytest.skipping This type was actually in `_pytest.skipping` previously, but was moved to `_pytest.mark.evaluate` in cf40c0743c565ed25bc14753e2350e010b39025a. I think the previous location was more appropriate, because the `MarkEvaluator` is not a generic mark facility, it is explicitly and exclusively used by the `skipif` and `xfail` marks to evaluate their particular set of arguments. So it is better to put it in the plugin code. Putting `skipping` related functionality into the core `_pytest.mark` module also causes some import cycles which we can avoid. --- src/_pytest/mark/evaluate.py | 124 --------------------------------- src/_pytest/skipping.py | 131 +++++++++++++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 130 deletions(-) delete mode 100644 src/_pytest/mark/evaluate.py diff --git a/src/_pytest/mark/evaluate.py b/src/_pytest/mark/evaluate.py deleted file mode 100644 index eb9903a5976..00000000000 --- a/src/_pytest/mark/evaluate.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -import platform -import sys -import traceback -from typing import Any -from typing import Dict -from typing import List -from typing import Optional - -from ..outcomes import fail -from ..outcomes import TEST_OUTCOME -from .structures import Mark -from _pytest.nodes import Item - - -def compiled_eval(expr: str, d: Dict[str, object]) -> Any: - import _pytest._code - - exprcode = _pytest._code.compile(expr, mode="eval") - return eval(exprcode, d) - - -class MarkEvaluator: - def __init__(self, item: Item, name: str) -> None: - self.item = item - self._marks = None # type: Optional[List[Mark]] - self._mark = None # type: Optional[Mark] - self._mark_name = name - - def __bool__(self) -> bool: - # don't cache here to prevent staleness - return bool(self._get_marks()) - - def wasvalid(self) -> bool: - return not hasattr(self, "exc") - - def _get_marks(self) -> List[Mark]: - return list(self.item.iter_markers(name=self._mark_name)) - - def invalidraise(self, exc) -> Optional[bool]: - raises = self.get("raises") - if not raises: - return None - return not isinstance(exc, raises) - - def istrue(self) -> bool: - try: - return self._istrue() - except TEST_OUTCOME: - self.exc = sys.exc_info() - if isinstance(self.exc[1], SyntaxError): - # TODO: Investigate why SyntaxError.offset is Optional, and if it can be None here. - assert self.exc[1].offset is not None - msg = [" " * (self.exc[1].offset + 4) + "^"] - msg.append("SyntaxError: invalid syntax") - else: - msg = traceback.format_exception_only(*self.exc[:2]) - fail( - "Error evaluating %r expression\n" - " %s\n" - "%s" % (self._mark_name, self.expr, "\n".join(msg)), - pytrace=False, - ) - - def _getglobals(self) -> Dict[str, object]: - d = {"os": os, "sys": sys, "platform": platform, "config": self.item.config} - if hasattr(self.item, "obj"): - d.update(self.item.obj.__globals__) # type: ignore[attr-defined] # noqa: F821 - return d - - def _istrue(self) -> bool: - if hasattr(self, "result"): - result = getattr(self, "result") # type: bool - return result - self._marks = self._get_marks() - - if self._marks: - self.result = False - for mark in self._marks: - self._mark = mark - if "condition" not in mark.kwargs: - args = mark.args - else: - args = (mark.kwargs["condition"],) - - for expr in args: - self.expr = expr - if isinstance(expr, str): - d = self._getglobals() - result = compiled_eval(expr, d) - else: - if "reason" not in mark.kwargs: - # XXX better be checked at collection time - msg = ( - "you need to specify reason=STRING " - "when using booleans as conditions." - ) - fail(msg) - result = bool(expr) - if result: - self.result = True - self.reason = mark.kwargs.get("reason", None) - self.expr = expr - return self.result - - if not args: - self.result = True - self.reason = mark.kwargs.get("reason", None) - return self.result - return False - - def get(self, attr, default=None): - if self._mark is None: - return default - return self._mark.kwargs.get(attr, default) - - def getexplanation(self): - expl = getattr(self, "reason", None) or self.get("reason", None) - if not expl: - if not hasattr(self, "expr"): - return "" - else: - return "condition: " + str(self.expr) - return expl diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 4e4b5a3c476..ee6b40daa77 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -1,25 +1,28 @@ """ support for skip/xfail functions and markers. """ +import os +import platform +import sys +import traceback +from typing import Any +from typing import Dict +from typing import List from typing import Optional from typing import Tuple from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser -from _pytest.mark.evaluate import MarkEvaluator +from _pytest.mark.structures import Mark from _pytest.nodes import Item from _pytest.outcomes import fail from _pytest.outcomes import skip +from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import xfail from _pytest.reports import BaseReport from _pytest.runner import CallInfo from _pytest.store import StoreKey -skipped_by_mark_key = StoreKey[bool]() -evalxfail_key = StoreKey[MarkEvaluator]() -unexpectedsuccess_key = StoreKey[str]() - - def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") group.addoption( @@ -79,6 +82,122 @@ def nop(*args, **kwargs): ) +def compiled_eval(expr: str, d: Dict[str, object]) -> Any: + import _pytest._code + + exprcode = _pytest._code.compile(expr, mode="eval") + return eval(exprcode, d) + + +class MarkEvaluator: + def __init__(self, item: Item, name: str) -> None: + self.item = item + self._marks = None # type: Optional[List[Mark]] + self._mark = None # type: Optional[Mark] + self._mark_name = name + + def __bool__(self) -> bool: + # don't cache here to prevent staleness + return bool(self._get_marks()) + + def wasvalid(self) -> bool: + return not hasattr(self, "exc") + + def _get_marks(self) -> List[Mark]: + return list(self.item.iter_markers(name=self._mark_name)) + + def invalidraise(self, exc) -> Optional[bool]: + raises = self.get("raises") + if not raises: + return None + return not isinstance(exc, raises) + + def istrue(self) -> bool: + try: + return self._istrue() + except TEST_OUTCOME: + self.exc = sys.exc_info() + if isinstance(self.exc[1], SyntaxError): + # TODO: Investigate why SyntaxError.offset is Optional, and if it can be None here. + assert self.exc[1].offset is not None + msg = [" " * (self.exc[1].offset + 4) + "^"] + msg.append("SyntaxError: invalid syntax") + else: + msg = traceback.format_exception_only(*self.exc[:2]) + fail( + "Error evaluating %r expression\n" + " %s\n" + "%s" % (self._mark_name, self.expr, "\n".join(msg)), + pytrace=False, + ) + + def _getglobals(self) -> Dict[str, object]: + d = {"os": os, "sys": sys, "platform": platform, "config": self.item.config} + if hasattr(self.item, "obj"): + d.update(self.item.obj.__globals__) # type: ignore[attr-defined] # noqa: F821 + return d + + def _istrue(self) -> bool: + if hasattr(self, "result"): + result = getattr(self, "result") # type: bool + return result + self._marks = self._get_marks() + + if self._marks: + self.result = False + for mark in self._marks: + self._mark = mark + if "condition" not in mark.kwargs: + args = mark.args + else: + args = (mark.kwargs["condition"],) + + for expr in args: + self.expr = expr + if isinstance(expr, str): + d = self._getglobals() + result = compiled_eval(expr, d) + else: + if "reason" not in mark.kwargs: + # XXX better be checked at collection time + msg = ( + "you need to specify reason=STRING " + "when using booleans as conditions." + ) + fail(msg) + result = bool(expr) + if result: + self.result = True + self.reason = mark.kwargs.get("reason", None) + self.expr = expr + return self.result + + if not args: + self.result = True + self.reason = mark.kwargs.get("reason", None) + return self.result + return False + + def get(self, attr, default=None): + if self._mark is None: + return default + return self._mark.kwargs.get(attr, default) + + def getexplanation(self): + expl = getattr(self, "reason", None) or self.get("reason", None) + if not expl: + if not hasattr(self, "expr"): + return "" + else: + return "condition: " + str(self.expr) + return expl + + +skipped_by_mark_key = StoreKey[bool]() +evalxfail_key = StoreKey[MarkEvaluator]() +unexpectedsuccess_key = StoreKey[str]() + + @hookimpl(tryfirst=True) def pytest_runtest_setup(item: Item) -> None: # Check if skip or skipif are specified as pytest marks From dd446bee5eb2d3ab0976309803dc77821eeac93e Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Fri, 19 Jun 2020 12:53:44 +0300 Subject: [PATCH 393/823] Fix exception causes all over the codebase --- AUTHORS | 1 + changelog/7383.bugfix.rst | 1 + src/_pytest/_code/source.py | 2 +- src/_pytest/config/__init__.py | 8 ++++---- src/_pytest/config/argparsing.py | 4 ++-- src/_pytest/config/findpaths.py | 2 +- src/_pytest/debugging.py | 6 +++--- src/_pytest/fixtures.py | 4 ++-- src/_pytest/logging.py | 4 ++-- src/_pytest/monkeypatch.py | 6 +++--- src/_pytest/python.py | 22 ++++++++++++---------- src/_pytest/warnings.py | 4 ++-- testing/test_config.py | 4 ++-- testing/test_runner.py | 4 ++-- 14 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 changelog/7383.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 821a7d8f41e..e4023768204 100644 --- a/AUTHORS +++ b/AUTHORS @@ -233,6 +233,7 @@ Pulkit Goyal Punyashloka Biswal Quentin Pradet Ralf Schmitt +Ram Rachum Ralph Giles Ran Benita Raphael Castaneda diff --git a/changelog/7383.bugfix.rst b/changelog/7383.bugfix.rst new file mode 100644 index 00000000000..d43106880cc --- /dev/null +++ b/changelog/7383.bugfix.rst @@ -0,0 +1 @@ +Fixed exception causes all over the codebase, i.e. use `raise new_exception from old_exception` when wrapping an exception. diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 3f732792fcf..2ccbaf657c2 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -215,7 +215,7 @@ def compile( # noqa: F811 newex.offset = ex.offset newex.lineno = ex.lineno newex.text = ex.text - raise newex + raise newex from ex else: if flag & ast.PyCF_ONLY_AST: assert isinstance(co, ast.AST) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 31b73a2c927..b4a5a70adc7 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1189,8 +1189,8 @@ def getini(self, name: str): def _getini(self, name: str) -> Any: try: description, type, default = self._parser._inidict[name] - except KeyError: - raise ValueError("unknown configuration value: {!r}".format(name)) + except KeyError as e: + raise ValueError("unknown configuration value: {!r}".format(name)) from e override_value = self._get_override_ini_value(name) if override_value is None: try: @@ -1286,14 +1286,14 @@ def getoption(self, name: str, default=notset, skip: bool = False): if val is None and skip: raise AttributeError(name) return val - except AttributeError: + except AttributeError as e: if default is not notset: return default if skip: import pytest pytest.skip("no {!r} option found".format(name)) - raise ValueError("no option named {!r}".format(name)) + raise ValueError("no option named {!r}".format(name)) from e def getvalue(self, name, path=None): """ (deprecated, use getoption()) """ diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 985a3fd1cd0..084ce16e59b 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -265,9 +265,9 @@ def __init__(self, *names: str, **attrs: Any) -> None: else: try: self.dest = self._short_opts[0][1:] - except IndexError: + except IndexError as e: self.dest = "???" # Needed for the error repr. - raise ArgumentError("need a long or short option", self) + raise ArgumentError("need a long or short option", self) from e def names(self) -> List[str]: return self._short_opts + self._long_opts diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index ae8c5f47f16..08a71122dcd 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -26,7 +26,7 @@ def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig: try: return iniconfig.IniConfig(path) except iniconfig.ParseError as exc: - raise UsageError(str(exc)) + raise UsageError(str(exc)) from exc def load_config_dict_from_file( diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 0567927c0d2..63126cbe02f 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -28,10 +28,10 @@ def _validate_usepdb_cls(value: str) -> Tuple[str, str]: """Validate syntax of --pdbcls option.""" try: modname, classname = value.split(":") - except ValueError: + except ValueError as e: raise argparse.ArgumentTypeError( "{!r} is not in the format 'modname:classname'".format(value) - ) + ) from e return (modname, classname) @@ -130,7 +130,7 @@ def _import_pdb_cls(cls, capman: "CaptureManager"): value = ":".join((modname, classname)) raise UsageError( "--pdbcls: could not import {!r}: {}".format(value, exc) - ) + ) from exc else: import pdb diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 05f0ecb6a47..4b2c6a7742c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -938,13 +938,13 @@ def _eval_scope_callable( # Type ignored because there is no typing mechanism to specify # keyword arguments, currently. result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg] # noqa: F821 - except Exception: + except Exception as e: raise TypeError( "Error evaluating {} while defining fixture '{}'.\n" "Expected a function with the signature (*, fixture_name, config)".format( scope_callable, fixture_name ) - ) + ) from e if not isinstance(result, str): fail( "Expected {} to return a 'str' while defining fixture '{}', but it returned:\n" diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 8755e5611a3..a06dc1ab553 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -487,13 +487,13 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i log_level = log_level.upper() try: return int(getattr(logging, log_level, log_level)) - except ValueError: + except ValueError as e: # Python logging does not recognise this as a logging level raise pytest.UsageError( "'{}' is not recognized as a logging level name for " "'{}'. Please consider passing the " "logging level num instead.".format(log_level, setting_name) - ) + ) from e # run after terminalreporter/capturemanager are configured diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 09f1ac36e52..2e5cca52628 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -73,7 +73,7 @@ def resolve(name: str) -> object: if expected == used: raise else: - raise ImportError("import error in {}: {}".format(used, ex)) + raise ImportError("import error in {}: {}".format(used, ex)) from ex found = annotated_getattr(found, part, used) return found @@ -81,12 +81,12 @@ def resolve(name: str) -> object: def annotated_getattr(obj: object, name: str, ann: str) -> object: try: obj = getattr(obj, name) - except AttributeError: + except AttributeError as e: raise AttributeError( "{!r} object at {} has no attribute {!r}".format( type(obj).__name__, ann, name ) - ) + ) from e return obj diff --git a/src/_pytest/python.py b/src/_pytest/python.py index bf45b8830e4..f3c42f42136 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -551,8 +551,10 @@ def _importtestmodule(self): importmode = self.config.getoption("--import-mode") try: mod = import_path(self.fspath, mode=importmode) - except SyntaxError: - raise self.CollectError(ExceptionInfo.from_current().getrepr(style="short")) + except SyntaxError as e: + raise self.CollectError( + ExceptionInfo.from_current().getrepr(style="short") + ) from e except ImportPathMismatchError as e: raise self.CollectError( "import file mismatch:\n" @@ -562,8 +564,8 @@ def _importtestmodule(self): " %s\n" "HINT: remove __pycache__ / .pyc files and/or use a " "unique basename for your test file modules" % e.args - ) - except ImportError: + ) from e + except ImportError as e: exc_info = ExceptionInfo.from_current() if self.config.getoption("verbose") < 2: exc_info.traceback = exc_info.traceback.filter(filter_traceback) @@ -578,7 +580,7 @@ def _importtestmodule(self): "Hint: make sure your test modules/packages have valid Python names.\n" "Traceback:\n" "{traceback}".format(fspath=self.fspath, traceback=formatted_tb) - ) + ) from e except _pytest.runner.Skipped as e: if e.allow_module_level: raise @@ -587,7 +589,7 @@ def _importtestmodule(self): "To decorate a test function, use the @pytest.mark.skip " "or @pytest.mark.skipif decorators instead, and to skip a " "module use `pytestmark = pytest.mark.{skip,skipif}." - ) + ) from e self.config.pluginmanager.consider_module(mod) return mod @@ -836,8 +838,8 @@ def _checkargnotcontained(self, arg: str) -> None: def getparam(self, name: str) -> object: try: return self.params[name] - except KeyError: - raise ValueError(name) + except KeyError as e: + raise ValueError(name) from e @property def id(self) -> str: @@ -1074,8 +1076,8 @@ def _validate_ids( except TypeError: try: iter(ids) - except TypeError: - raise TypeError("ids must be a callable or an iterable") + except TypeError as e: + raise TypeError("ids must be a callable or an iterable") from e num_ids = len(parameters) # num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849 diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 5cedba2442a..f92350b20d7 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -48,8 +48,8 @@ def _parse_filter( lineno = int(lineno_) if lineno < 0: raise ValueError - except (ValueError, OverflowError): - raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) + except (ValueError, OverflowError) as e: + raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e else: lineno = 0 return (action, message, category, module, lineno) diff --git a/testing/test_config.py b/testing/test_config.py index c9eea7a1675..4e64a6928a8 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1778,5 +1778,5 @@ def test_conftest_import_error_repr(tmpdir): ): try: raise RuntimeError("some error") - except Exception: - raise ConftestImportFailure(path, sys.exc_info()) + except Exception as e: + raise ConftestImportFailure(path, sys.exc_info()) from e diff --git a/testing/test_runner.py b/testing/test_runner.py index 9c19ded0e70..474ff4df817 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -534,8 +534,8 @@ def test_outcomeexception_passes_except_Exception() -> None: with pytest.raises(outcomes.OutcomeException): try: raise outcomes.OutcomeException("test") - except Exception: - raise NotImplementedError() + except Exception as e: + raise NotImplementedError from e def test_pytest_exit() -> None: From 3e6fe92b7ea3c120e8024a970bf37a7c6c137714 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 19 Jun 2020 13:33:55 +0300 Subject: [PATCH 394/823] skipping: refactor skipif/xfail mark evaluation Previously, skipif/xfail marks were evaluated using a `MarkEvaluator` class. I found this class very difficult to understand. Instead of `MarkEvaluator`, rewrite using straight functions which are hopefully easier to follow. I tried to keep the semantics exactly as before, except improving a few error messages. --- src/_pytest/skipping.py | 336 ++++++++++++++++++++------------------- testing/test_skipping.py | 140 ++++++++-------- 2 files changed, 247 insertions(+), 229 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index ee6b40daa77..894eda49987 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -3,12 +3,13 @@ import platform import sys import traceback -from typing import Any -from typing import Dict -from typing import List from typing import Optional from typing import Tuple +import attr + +import _pytest._code +from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser @@ -16,12 +17,14 @@ from _pytest.nodes import Item from _pytest.outcomes import fail from _pytest.outcomes import skip -from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import xfail from _pytest.reports import BaseReport from _pytest.runner import CallInfo from _pytest.store import StoreKey +if TYPE_CHECKING: + from typing import Type + def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") @@ -64,17 +67,16 @@ def nop(*args, **kwargs): ) config.addinivalue_line( "markers", - "skipif(condition): skip the given test function if eval(condition) " - "results in a True value. Evaluation happens within the " - "module global context. Example: skipif('sys.platform == \"win32\"') " - "skips the test if we are on the win32 platform. see " - "https://docs.pytest.org/en/latest/skipping.html", + "skipif(condition, ..., *, reason=...): " + "skip the given test function if any of the conditions evaluate to True. " + "Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. " + "see https://docs.pytest.org/en/latest/skipping.html", ) config.addinivalue_line( "markers", - "xfail(condition, reason=None, run=True, raises=None, strict=False): " - "mark the test function as an expected failure if eval(condition) " - "has a True value. Optionally specify a reason for better reporting " + "xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): " + "mark the test function as an expected failure if any of the conditions " + "evaluate to True. Optionally specify a reason for better reporting " "and run=False if you don't even want to execute the test function. " "If only specific exception(s) are expected, you can list them in " "raises, and if the test fails in other ways, it will be reported as " @@ -82,179 +84,191 @@ def nop(*args, **kwargs): ) -def compiled_eval(expr: str, d: Dict[str, object]) -> Any: - import _pytest._code +def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, str]: + """Evaluate a single skipif/xfail condition. + + If an old-style string condition is given, it is eval()'d, otherwise the + condition is bool()'d. If this fails, an appropriately formatted pytest.fail + is raised. + + Returns (result, reason). The reason is only relevant if the result is True. + """ + # String condition. + if isinstance(condition, str): + globals_ = { + "os": os, + "sys": sys, + "platform": platform, + "config": item.config, + } + if hasattr(item, "obj"): + globals_.update(item.obj.__globals__) # type: ignore[attr-defined] + try: + condition_code = _pytest._code.compile(condition, mode="eval") + result = eval(condition_code, globals_) + except SyntaxError as exc: + msglines = [ + "Error evaluating %r condition" % mark.name, + " " + condition, + " " + " " * (exc.offset or 0) + "^", + "SyntaxError: invalid syntax", + ] + fail("\n".join(msglines), pytrace=False) + except Exception as exc: + msglines = [ + "Error evaluating %r condition" % mark.name, + " " + condition, + *traceback.format_exception_only(type(exc), exc), + ] + fail("\n".join(msglines), pytrace=False) + + # Boolean condition. + else: + try: + result = bool(condition) + except Exception as exc: + msglines = [ + "Error evaluating %r condition as a boolean" % mark.name, + *traceback.format_exception_only(type(exc), exc), + ] + fail("\n".join(msglines), pytrace=False) + + reason = mark.kwargs.get("reason", None) + if reason is None: + if isinstance(condition, str): + reason = "condition: " + condition + else: + # XXX better be checked at collection time + msg = ( + "Error evaluating %r: " % mark.name + + "you need to specify reason=STRING when using booleans as conditions." + ) + fail(msg, pytrace=False) + + return result, reason + + +@attr.s(slots=True, frozen=True) +class Skip: + """The result of evaluate_skip_marks().""" - exprcode = _pytest._code.compile(expr, mode="eval") - return eval(exprcode, d) + reason = attr.ib(type=str) -class MarkEvaluator: - def __init__(self, item: Item, name: str) -> None: - self.item = item - self._marks = None # type: Optional[List[Mark]] - self._mark = None # type: Optional[Mark] - self._mark_name = name +def evaluate_skip_marks(item: Item) -> Optional[Skip]: + """Evaluate skip and skipif marks on item, returning Skip if triggered.""" + for mark in item.iter_markers(name="skipif"): + if "condition" not in mark.kwargs: + conditions = mark.args + else: + conditions = (mark.kwargs["condition"],) + + # Unconditional. + if not conditions: + reason = mark.kwargs.get("reason", "") + return Skip(reason) + + # If any of the conditions are true. + for condition in conditions: + result, reason = evaluate_condition(item, mark, condition) + if result: + return Skip(reason) + + for mark in item.iter_markers(name="skip"): + if "reason" in mark.kwargs: + reason = mark.kwargs["reason"] + elif mark.args: + reason = mark.args[0] + else: + reason = "unconditional skip" + return Skip(reason) - def __bool__(self) -> bool: - # don't cache here to prevent staleness - return bool(self._get_marks()) + return None - def wasvalid(self) -> bool: - return not hasattr(self, "exc") - def _get_marks(self) -> List[Mark]: - return list(self.item.iter_markers(name=self._mark_name)) +@attr.s(slots=True, frozen=True) +class Xfail: + """The result of evaluate_xfail_marks().""" - def invalidraise(self, exc) -> Optional[bool]: - raises = self.get("raises") - if not raises: - return None - return not isinstance(exc, raises) + reason = attr.ib(type=str) + run = attr.ib(type=bool) + strict = attr.ib(type=bool) + raises = attr.ib(type=Optional[Tuple["Type[BaseException]", ...]]) - def istrue(self) -> bool: - try: - return self._istrue() - except TEST_OUTCOME: - self.exc = sys.exc_info() - if isinstance(self.exc[1], SyntaxError): - # TODO: Investigate why SyntaxError.offset is Optional, and if it can be None here. - assert self.exc[1].offset is not None - msg = [" " * (self.exc[1].offset + 4) + "^"] - msg.append("SyntaxError: invalid syntax") - else: - msg = traceback.format_exception_only(*self.exc[:2]) - fail( - "Error evaluating %r expression\n" - " %s\n" - "%s" % (self._mark_name, self.expr, "\n".join(msg)), - pytrace=False, - ) - def _getglobals(self) -> Dict[str, object]: - d = {"os": os, "sys": sys, "platform": platform, "config": self.item.config} - if hasattr(self.item, "obj"): - d.update(self.item.obj.__globals__) # type: ignore[attr-defined] # noqa: F821 - return d - - def _istrue(self) -> bool: - if hasattr(self, "result"): - result = getattr(self, "result") # type: bool - return result - self._marks = self._get_marks() - - if self._marks: - self.result = False - for mark in self._marks: - self._mark = mark - if "condition" not in mark.kwargs: - args = mark.args - else: - args = (mark.kwargs["condition"],) - - for expr in args: - self.expr = expr - if isinstance(expr, str): - d = self._getglobals() - result = compiled_eval(expr, d) - else: - if "reason" not in mark.kwargs: - # XXX better be checked at collection time - msg = ( - "you need to specify reason=STRING " - "when using booleans as conditions." - ) - fail(msg) - result = bool(expr) - if result: - self.result = True - self.reason = mark.kwargs.get("reason", None) - self.expr = expr - return self.result - - if not args: - self.result = True - self.reason = mark.kwargs.get("reason", None) - return self.result - return False - - def get(self, attr, default=None): - if self._mark is None: - return default - return self._mark.kwargs.get(attr, default) - - def getexplanation(self): - expl = getattr(self, "reason", None) or self.get("reason", None) - if not expl: - if not hasattr(self, "expr"): - return "" - else: - return "condition: " + str(self.expr) - return expl +def evaluate_xfail_marks(item: Item) -> Optional[Xfail]: + """Evaluate xfail marks on item, returning Xfail if triggered.""" + for mark in item.iter_markers(name="xfail"): + run = mark.kwargs.get("run", True) + strict = mark.kwargs.get("strict", item.config.getini("xfail_strict")) + raises = mark.kwargs.get("raises", None) + if "condition" not in mark.kwargs: + conditions = mark.args + else: + conditions = (mark.kwargs["condition"],) + + # Unconditional. + if not conditions: + reason = mark.kwargs.get("reason", "") + return Xfail(reason, run, strict, raises) + # If any of the conditions are true. + for condition in conditions: + result, reason = evaluate_condition(item, mark, condition) + if result: + return Xfail(reason, run, strict, raises) + return None + + +# Whether skipped due to skip or skipif marks. skipped_by_mark_key = StoreKey[bool]() -evalxfail_key = StoreKey[MarkEvaluator]() +# Saves the xfail mark evaluation. Can be refreshed during call if None. +xfailed_key = StoreKey[Optional[Xfail]]() unexpectedsuccess_key = StoreKey[str]() @hookimpl(tryfirst=True) def pytest_runtest_setup(item: Item) -> None: - # Check if skip or skipif are specified as pytest marks item._store[skipped_by_mark_key] = False - eval_skipif = MarkEvaluator(item, "skipif") - if eval_skipif.istrue(): - item._store[skipped_by_mark_key] = True - skip(eval_skipif.getexplanation()) - for skip_info in item.iter_markers(name="skip"): + skipped = evaluate_skip_marks(item) + if skipped: item._store[skipped_by_mark_key] = True - if "reason" in skip_info.kwargs: - skip(skip_info.kwargs["reason"]) - elif skip_info.args: - skip(skip_info.args[0]) - else: - skip("unconditional skip") + skip(skipped.reason) - item._store[evalxfail_key] = MarkEvaluator(item, "xfail") - check_xfail_no_run(item) + if not item.config.option.runxfail: + item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) + if xfailed and not xfailed.run: + xfail("[NOTRUN] " + xfailed.reason) @hookimpl(hookwrapper=True) def pytest_runtest_call(item: Item): - check_xfail_no_run(item) - outcome = yield - passed = outcome.excinfo is None - if passed: - check_strict_xfail(item) - - -def check_xfail_no_run(item: Item) -> None: - """check xfail(run=False)""" if not item.config.option.runxfail: - evalxfail = item._store[evalxfail_key] - if evalxfail.istrue(): - if not evalxfail.get("run", True): - xfail("[NOTRUN] " + evalxfail.getexplanation()) + xfailed = item._store.get(xfailed_key, None) + if xfailed is None: + item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) + if xfailed and not xfailed.run: + xfail("[NOTRUN] " + xfailed.reason) + outcome = yield + passed = outcome.excinfo is None -def check_strict_xfail(item: Item) -> None: - """check xfail(strict=True) for the given PASSING test""" - evalxfail = item._store[evalxfail_key] - if evalxfail.istrue(): - strict_default = item.config.getini("xfail_strict") - is_strict_xfail = evalxfail.get("strict", strict_default) - if is_strict_xfail: - del item._store[evalxfail_key] - explanation = evalxfail.getexplanation() - fail("[XPASS(strict)] " + explanation, pytrace=False) + if passed: + xfailed = item._store.get(xfailed_key, None) + if xfailed is None: + item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) + if xfailed and xfailed.strict: + del item._store[xfailed_key] + fail("[XPASS(strict)] " + xfailed.reason, pytrace=False) @hookimpl(hookwrapper=True) def pytest_runtest_makereport(item: Item, call: CallInfo[None]): outcome = yield rep = outcome.get_result() - evalxfail = item._store.get(evalxfail_key, None) + xfailed = item._store.get(xfailed_key, None) # unittest special case, see setting of unexpectedsuccess_key if unexpectedsuccess_key in item._store and rep.when == "call": reason = item._store[unexpectedsuccess_key] @@ -263,30 +277,27 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): else: rep.longrepr = "Unexpected success" rep.outcome = "failed" - elif item.config.option.runxfail: pass # don't interfere elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception): assert call.excinfo.value.msg is not None rep.wasxfail = "reason: " + call.excinfo.value.msg rep.outcome = "skipped" - elif evalxfail and not rep.skipped and evalxfail.wasvalid() and evalxfail.istrue(): + elif not rep.skipped and xfailed: if call.excinfo: - if evalxfail.invalidraise(call.excinfo.value): + raises = xfailed.raises + if raises is not None and not isinstance(call.excinfo.value, raises): rep.outcome = "failed" else: rep.outcome = "skipped" - rep.wasxfail = evalxfail.getexplanation() + rep.wasxfail = xfailed.reason elif call.when == "call": - strict_default = item.config.getini("xfail_strict") - is_strict_xfail = evalxfail.get("strict", strict_default) - explanation = evalxfail.getexplanation() - if is_strict_xfail: + if xfailed.strict: rep.outcome = "failed" - rep.longrepr = "[XPASS(strict)] {}".format(explanation) + rep.longrepr = "[XPASS(strict)] " + xfailed.reason else: rep.outcome = "passed" - rep.wasxfail = explanation + rep.wasxfail = xfailed.reason elif ( item._store.get(skipped_by_mark_key, True) and rep.skipped @@ -301,9 +312,6 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): rep.longrepr = str(filename), line + 1, reason -# called by terminalreporter progress reporting - - def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: if hasattr(report, "wasxfail"): if report.skipped: diff --git a/testing/test_skipping.py b/testing/test_skipping.py index a6f1a9c09b5..0b1c0b49b03 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -2,68 +2,74 @@ import pytest from _pytest.runner import runtestprotocol -from _pytest.skipping import MarkEvaluator +from _pytest.skipping import evaluate_skip_marks +from _pytest.skipping import evaluate_xfail_marks from _pytest.skipping import pytest_runtest_setup -class TestEvaluator: +class TestEvaluation: def test_no_marker(self, testdir): item = testdir.getitem("def test_func(): pass") - evalskipif = MarkEvaluator(item, "skipif") - assert not evalskipif - assert not evalskipif.istrue() + skipped = evaluate_skip_marks(item) + assert not skipped - def test_marked_no_args(self, testdir): + def test_marked_xfail_no_args(self, testdir): item = testdir.getitem( """ import pytest - @pytest.mark.xyz + @pytest.mark.xfail + def test_func(): + pass + """ + ) + xfailed = evaluate_xfail_marks(item) + assert xfailed + assert xfailed.reason == "" + assert xfailed.run + + def test_marked_skipif_no_args(self, testdir): + item = testdir.getitem( + """ + import pytest + @pytest.mark.skipif def test_func(): pass """ ) - ev = MarkEvaluator(item, "xyz") - assert ev - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "" - assert not ev.get("run", False) + skipped = evaluate_skip_marks(item) + assert skipped + assert skipped.reason == "" def test_marked_one_arg(self, testdir): item = testdir.getitem( """ import pytest - @pytest.mark.xyz("hasattr(os, 'sep')") + @pytest.mark.skipif("hasattr(os, 'sep')") def test_func(): pass """ ) - ev = MarkEvaluator(item, "xyz") - assert ev - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "condition: hasattr(os, 'sep')" + skipped = evaluate_skip_marks(item) + assert skipped + assert skipped.reason == "condition: hasattr(os, 'sep')" def test_marked_one_arg_with_reason(self, testdir): item = testdir.getitem( """ import pytest - @pytest.mark.xyz("hasattr(os, 'sep')", attr=2, reason="hello world") + @pytest.mark.skipif("hasattr(os, 'sep')", attr=2, reason="hello world") def test_func(): pass """ ) - ev = MarkEvaluator(item, "xyz") - assert ev - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "hello world" - assert ev.get("attr") == 2 + skipped = evaluate_skip_marks(item) + assert skipped + assert skipped.reason == "hello world" def test_marked_one_arg_twice(self, testdir): lines = [ """@pytest.mark.skipif("not hasattr(os, 'murks')")""", - """@pytest.mark.skipif("hasattr(os, 'murks')")""", + """@pytest.mark.skipif(condition="hasattr(os, 'murks')")""", ] for i in range(0, 2): item = testdir.getitem( @@ -76,11 +82,9 @@ def test_func(): """ % (lines[i], lines[(i + 1) % 2]) ) - ev = MarkEvaluator(item, "skipif") - assert ev - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "condition: not hasattr(os, 'murks')" + skipped = evaluate_skip_marks(item) + assert skipped + assert skipped.reason == "condition: not hasattr(os, 'murks')" def test_marked_one_arg_twice2(self, testdir): item = testdir.getitem( @@ -92,13 +96,11 @@ def test_func(): pass """ ) - ev = MarkEvaluator(item, "skipif") - assert ev - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "condition: not hasattr(os, 'murks')" + skipped = evaluate_skip_marks(item) + assert skipped + assert skipped.reason == "condition: not hasattr(os, 'murks')" - def test_marked_skip_with_not_string(self, testdir) -> None: + def test_marked_skipif_with_boolean_without_reason(self, testdir) -> None: item = testdir.getitem( """ import pytest @@ -107,14 +109,34 @@ def test_func(): pass """ ) - ev = MarkEvaluator(item, "skipif") - exc = pytest.raises(pytest.fail.Exception, ev.istrue) - assert exc.value.msg is not None + with pytest.raises(pytest.fail.Exception) as excinfo: + evaluate_skip_marks(item) + assert excinfo.value.msg is not None assert ( - """Failed: you need to specify reason=STRING when using booleans as conditions.""" - in exc.value.msg + """Error evaluating 'skipif': you need to specify reason=STRING when using booleans as conditions.""" + in excinfo.value.msg ) + def test_marked_skipif_with_invalid_boolean(self, testdir) -> None: + item = testdir.getitem( + """ + import pytest + + class InvalidBool: + def __bool__(self): + raise TypeError("INVALID") + + @pytest.mark.skipif(InvalidBool(), reason="xxx") + def test_func(): + pass + """ + ) + with pytest.raises(pytest.fail.Exception) as excinfo: + evaluate_skip_marks(item) + assert excinfo.value.msg is not None + assert "Error evaluating 'skipif' condition as a boolean" in excinfo.value.msg + assert "INVALID" in excinfo.value.msg + def test_skipif_class(self, testdir): (item,) = testdir.getitems( """ @@ -126,10 +148,9 @@ def test_func(self): """ ) item.config._hackxyz = 3 - ev = MarkEvaluator(item, "skipif") - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "condition: config._hackxyz" + skipped = evaluate_skip_marks(item) + assert skipped + assert skipped.reason == "condition: config._hackxyz" class TestXFail: @@ -895,10 +916,10 @@ def test_func(): result.stdout.fnmatch_lines( [ "*ERROR*test_nameerror*", - "*evaluating*skipif*expression*", + "*evaluating*skipif*condition*", "*asd*", "*ERROR*test_syntax*", - "*evaluating*xfail*expression*", + "*evaluating*xfail*condition*", " syntax error", markline, "SyntaxError: invalid syntax", @@ -924,25 +945,12 @@ def test_boolean(): result.stdout.fnmatch_lines(["*SKIP*x == 3*", "*XFAIL*test_boolean*", "*x == 3*"]) -def test_direct_gives_error(testdir): - testdir.makepyfile( - """ - import pytest - @pytest.mark.skipif(True) - def test_skip1(): - pass - """ - ) - result = testdir.runpytest() - result.stdout.fnmatch_lines(["*1 error*"]) - - def test_default_markers(testdir): result = testdir.runpytest("--markers") result.stdout.fnmatch_lines( [ - "*skipif(*condition)*skip*", - "*xfail(*condition, reason=None, run=True, raises=None, strict=False)*expected failure*", + "*skipif(condition, ..., [*], reason=...)*skip*", + "*xfail(condition, ..., [*], reason=..., run=True, raises=None, strict=xfail_strict)*expected failure*", ] ) @@ -1137,7 +1145,9 @@ def test_mark_xfail_item(testdir): class MyItem(pytest.Item): nodeid = 'foo' def setup(self): - marker = pytest.mark.xfail(True, reason="Expected failure") + marker = pytest.mark.xfail("1 == 2", reason="Expected failure - false") + self.add_marker(marker) + marker = pytest.mark.xfail(True, reason="Expected failure - true") self.add_marker(marker) def runtest(self): assert False From ac89d6532a8c1f652f6a68c0b9caad80cde0042f Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 20 Jun 2020 12:15:58 -0400 Subject: [PATCH 395/823] replace stderr warnings with the warnings module --- changelog/7295.trivial.rst | 1 + src/_pytest/config/__init__.py | 48 ++++++++++++++++++++-------------- testing/test_assertion.py | 14 ++++++++-- testing/test_config.py | 16 +++++++----- 4 files changed, 51 insertions(+), 28 deletions(-) create mode 100644 changelog/7295.trivial.rst diff --git a/changelog/7295.trivial.rst b/changelog/7295.trivial.rst new file mode 100644 index 00000000000..113a7ee605f --- /dev/null +++ b/changelog/7295.trivial.rst @@ -0,0 +1 @@ +``src/_pytest/config/__init__.py`` now uses the ``warnings`` module to report warnings instead of ``sys.stderr.write``. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 31b73a2c927..45e0c05ea13 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -992,7 +992,7 @@ def _consider_importhook(self, args: Sequence[str]) -> None: mode = "plain" else: self._mark_plugins_for_rewrite(hook) - _warn_about_missing_assertion(mode) + self._warn_about_missing_assertion(mode) def _mark_plugins_for_rewrite(self, hook) -> None: """ @@ -1136,7 +1136,12 @@ def _validate_plugins(self) -> None: def _warn_or_fail_if_strict(self, message: str) -> None: if self.known_args_namespace.strict_config: fail(message, pytrace=False) - sys.stderr.write("WARNING: {}".format(message)) + + from _pytest.warnings import _issue_warning_captured + + _issue_warning_captured( + PytestConfigWarning(message), self.hook, stacklevel=2, + ) def _get_unknown_ini_keys(self) -> List[str]: parser_inicfg = self._parser._inidict @@ -1303,6 +1308,27 @@ def getvalueorskip(self, name, path=None): """ (deprecated, use getoption(skip=True)) """ return self.getoption(name, skip=True) + def _warn_about_missing_assertion(self, mode: str) -> None: + if not _assertion_supported(): + from _pytest.warnings import _issue_warning_captured + + warning_text = ( + "assertions not in test modules or" + " plugins will be ignored" + " because assert statements are not executed " + "by the underlying Python interpreter " + "(are you using python -O?)\n" + ) + if mode == "plain": + warning_text = ( + "ASSERTIONS ARE NOT EXECUTED" + " and FAILING TESTS WILL PASS. Are you" + " using python -O?" + ) + _issue_warning_captured( + PytestConfigWarning(warning_text), self.hook, stacklevel=2, + ) + def _assertion_supported(): try: @@ -1313,24 +1339,6 @@ def _assertion_supported(): return False -def _warn_about_missing_assertion(mode): - if not _assertion_supported(): - if mode == "plain": - sys.stderr.write( - "WARNING: ASSERTIONS ARE NOT EXECUTED" - " and FAILING TESTS WILL PASS. Are you" - " using python -O?" - ) - else: - sys.stderr.write( - "WARNING: assertions not in test modules or" - " plugins will be ignored" - " because assert statements are not executed " - "by the underlying Python interpreter " - "(are you using python -O?)\n" - ) - - def create_terminal_writer(config: Config, *args, **kwargs) -> TerminalWriter: """Create a TerminalWriter instance configured according to the options in the config object. Every code which requires a TerminalWriter object diff --git a/testing/test_assertion.py b/testing/test_assertion.py index ae5e75dbfbb..5dbae96a0d2 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1385,11 +1385,21 @@ def test_multitask_job(): @pytest.mark.skipif("'__pypy__' in sys.builtin_module_names") def test_warn_missing(testdir): + testdir.makepyfile("") + + warning_output = [ + "warning :*PytestConfigWarning:*assert statements are not executed*" + ] result = testdir.run(sys.executable, "-OO", "-m", "pytest", "-h") - result.stderr.fnmatch_lines(["*WARNING*assert statements are not executed*"]) + result.stdout.fnmatch_lines(warning_output) + + warning_output = [ + "=*= warnings summary =*=", + "*PytestConfigWarning:*assert statements are not executed*", + ] result = testdir.run(sys.executable, "-OO", "-m", "pytest") - result.stderr.fnmatch_lines(["*WARNING*assert statements are not executed*"]) + result.stdout.fnmatch_lines(warning_output) def test_recursion_source_decode(testdir): diff --git a/testing/test_config.py b/testing/test_config.py index c9eea7a1675..9303662326d 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -163,7 +163,7 @@ def test_confcutdir(self, testdir): assert result.ret == 0 @pytest.mark.parametrize( - "ini_file_text, invalid_keys, stderr_output, exception_text", + "ini_file_text, invalid_keys, warning_output, exception_text", [ ( """ @@ -173,8 +173,9 @@ def test_confcutdir(self, testdir): """, ["unknown_ini", "another_unknown_ini"], [ - "WARNING: Unknown config ini key: another_unknown_ini", - "WARNING: Unknown config ini key: unknown_ini", + "=*= warnings summary =*=", + "*PytestConfigWarning:*Unknown config ini key: another_unknown_ini", + "*PytestConfigWarning:*Unknown config ini key: unknown_ini", ], "Unknown config ini key: another_unknown_ini", ), @@ -185,7 +186,10 @@ def test_confcutdir(self, testdir): minversion = 5.0.0 """, ["unknown_ini"], - ["WARNING: Unknown config ini key: unknown_ini"], + [ + "=*= warnings summary =*=", + "*PytestConfigWarning:*Unknown config ini key: unknown_ini", + ], "Unknown config ini key: unknown_ini", ), ( @@ -220,7 +224,7 @@ def test_confcutdir(self, testdir): ], ) def test_invalid_ini_keys( - self, testdir, ini_file_text, invalid_keys, stderr_output, exception_text + self, testdir, ini_file_text, invalid_keys, warning_output, exception_text ): testdir.makeconftest( """ @@ -234,7 +238,7 @@ def pytest_addoption(parser): assert sorted(config._get_unknown_ini_keys()) == sorted(invalid_keys) result = testdir.runpytest() - result.stderr.fnmatch_lines(stderr_output) + result.stdout.fnmatch_lines(warning_output) if exception_text: with pytest.raises(pytest.fail.Exception, match=exception_text): From a9d50aeab671bd67d58abb19a1839736cb4a7966 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 20 Jun 2020 12:18:55 -0400 Subject: [PATCH 396/823] remove extra whitespace --- testing/test_assertion.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 5dbae96a0d2..f28a51f96a4 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1385,7 +1385,6 @@ def test_multitask_job(): @pytest.mark.skipif("'__pypy__' in sys.builtin_module_names") def test_warn_missing(testdir): - testdir.makepyfile("") warning_output = [ From fe68c5869866149b1c00d6280f3e491883d0b7e9 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 20 Jun 2020 13:06:41 -0400 Subject: [PATCH 397/823] add test_warn_missing case for --assert=plain --- testing/test_assertion.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index f28a51f96a4..64a94941a2e 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1400,6 +1400,13 @@ def test_warn_missing(testdir): result = testdir.run(sys.executable, "-OO", "-m", "pytest") result.stdout.fnmatch_lines(warning_output) + warning_output = [ + "=*= warnings summary =*=", + "*PytestConfigWarning: ASSERTIONS ARE NOT EXECUTED and FAILING TESTS WILL PASS. Are you using python -O?", + ] + result = testdir.run(sys.executable, "-OO", "-m", "pytest", "--assert=plain") + result.stdout.fnmatch_lines(warning_output) + def test_recursion_source_decode(testdir): testdir.makepyfile( From 33de350619cf541476d1ab987377f9bc2f06179f Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 21 Jun 2020 10:26:36 -0400 Subject: [PATCH 398/823] parametrize test_warn_missing for a cleaner test --- testing/test_assertion.py | 45 ++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 64a94941a2e..5c9bb35fadc 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1384,27 +1384,34 @@ def test_multitask_job(): @pytest.mark.skipif("'__pypy__' in sys.builtin_module_names") -def test_warn_missing(testdir): +@pytest.mark.parametrize( + "cmdline_args, warning_output", + [ + ( + ["-OO", "-m", "pytest", "-h"], + ["warning :*PytestConfigWarning:*assert statements are not executed*"], + ), + ( + ["-OO", "-m", "pytest"], + [ + "=*= warnings summary =*=", + "*PytestConfigWarning:*assert statements are not executed*", + ], + ), + ( + ["-OO", "-m", "pytest", "--assert=plain"], + [ + "=*= warnings summary =*=", + "*PytestConfigWarning: ASSERTIONS ARE NOT EXECUTED and FAILING TESTS WILL PASS. " + "Are you using python -O?", + ], + ), + ], +) +def test_warn_missing(testdir, cmdline_args, warning_output): testdir.makepyfile("") - warning_output = [ - "warning :*PytestConfigWarning:*assert statements are not executed*" - ] - result = testdir.run(sys.executable, "-OO", "-m", "pytest", "-h") - result.stdout.fnmatch_lines(warning_output) - - warning_output = [ - "=*= warnings summary =*=", - "*PytestConfigWarning:*assert statements are not executed*", - ] - result = testdir.run(sys.executable, "-OO", "-m", "pytest") - result.stdout.fnmatch_lines(warning_output) - - warning_output = [ - "=*= warnings summary =*=", - "*PytestConfigWarning: ASSERTIONS ARE NOT EXECUTED and FAILING TESTS WILL PASS. Are you using python -O?", - ] - result = testdir.run(sys.executable, "-OO", "-m", "pytest", "--assert=plain") + result = testdir.run(sys.executable, *cmdline_args) result.stdout.fnmatch_lines(warning_output) From c9737ae914891027da5f0bd39494dd51a3b3f19f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 20 Jun 2020 15:59:29 +0300 Subject: [PATCH 399/823] skipping: simplify xfail handling during call phase There is no need to do the XPASS check here, pytest_runtest_makereport already handled that (the current handling there is dead code). All the hook needs to do is refresh the xfail evaluation if needed, and check the NOTRUN condition again. --- src/_pytest/skipping.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 894eda49987..7fc43fce193 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -3,6 +3,7 @@ import platform import sys import traceback +from typing import Generator from typing import Optional from typing import Tuple @@ -244,24 +245,16 @@ def pytest_runtest_setup(item: Item) -> None: @hookimpl(hookwrapper=True) -def pytest_runtest_call(item: Item): +def pytest_runtest_call(item: Item) -> Generator[None, None, None]: + xfailed = item._store.get(xfailed_key, None) + if xfailed is None: + item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) + if not item.config.option.runxfail: - xfailed = item._store.get(xfailed_key, None) - if xfailed is None: - item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) if xfailed and not xfailed.run: xfail("[NOTRUN] " + xfailed.reason) - outcome = yield - passed = outcome.excinfo is None - - if passed: - xfailed = item._store.get(xfailed_key, None) - if xfailed is None: - item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) - if xfailed and xfailed.strict: - del item._store[xfailed_key] - fail("[XPASS(strict)] " + xfailed.reason, pytrace=False) + yield @hookimpl(hookwrapper=True) From 7d8d1b4440028660c81ca242968df89e8c6b896e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 21 Jun 2020 20:14:45 +0300 Subject: [PATCH 400/823] skipping: better links in --markers output Suggested by Bruno. --- src/_pytest/skipping.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 7fc43fce193..7bd975e5a09 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -71,7 +71,7 @@ def nop(*args, **kwargs): "skipif(condition, ..., *, reason=...): " "skip the given test function if any of the conditions evaluate to True. " "Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. " - "see https://docs.pytest.org/en/latest/skipping.html", + "See https://docs.pytest.org/en/stable/reference.html#pytest-mark-skipif", ) config.addinivalue_line( "markers", @@ -81,7 +81,7 @@ def nop(*args, **kwargs): "and run=False if you don't even want to execute the test function. " "If only specific exception(s) are expected, you can list them in " "raises, and if the test fails in other ways, it will be reported as " - "a true failure. See https://docs.pytest.org/en/latest/skipping.html", + "a true failure. See https://docs.pytest.org/en/stable/reference.html#pytest-mark-xfail", ) From b3fb5a2d47743a09c551555da22da27ce9e73f41 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 16 Jun 2020 21:16:04 +0300 Subject: [PATCH 401/823] Type annotate pytest.mark.* builtin marks --- src/_pytest/mark/structures.py | 100 +++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 10 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 3d512816c7c..2d756bb420a 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -46,11 +46,19 @@ def get_empty_parameterset_mark( ) -> "MarkDecorator": from ..nodes import Collector + fs, lineno = getfslineno(func) + reason = "got empty parameter set %r, function %s at %s:%d" % ( + argnames, + func.__name__, + fs, + lineno, + ) + requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) if requested_mark in ("", None, "skip"): - mark = MARK_GEN.skip + mark = MARK_GEN.skip(reason=reason) elif requested_mark == "xfail": - mark = MARK_GEN.xfail(run=False) + mark = MARK_GEN.xfail(reason=reason, run=False) elif requested_mark == "fail_at_collect": f_name = func.__name__ _, lineno = getfslineno(func) @@ -59,14 +67,7 @@ def get_empty_parameterset_mark( ) else: raise LookupError(requested_mark) - fs, lineno = getfslineno(func) - reason = "got empty parameter set %r, function %s at %s:%d" % ( - argnames, - func.__name__, - fs, - lineno, - ) - return mark(reason=reason) + return mark class ParameterSet( @@ -379,6 +380,76 @@ def store_mark(obj, mark: Mark) -> None: obj.pytestmark = get_unpacked_marks(obj) + [mark] +# Typing for builtin pytest marks. This is cheating; it gives builtin marks +# special privilege, and breaks modularity. But practicality beats purity... +if TYPE_CHECKING: + from _pytest.fixtures import _Scope + + class _SkipMarkDecorator(MarkDecorator): + @overload # type: ignore[override,misc] + def __call__(self, arg: _Markable) -> _Markable: + raise NotImplementedError() + + @overload # noqa: F811 + def __call__(self, reason: str = ...) -> "MarkDecorator": # noqa: F811 + raise NotImplementedError() + + class _SkipifMarkDecorator(MarkDecorator): + def __call__( # type: ignore[override] + self, + condition: Union[str, bool] = ..., + *conditions: Union[str, bool], + reason: str = ... + ) -> MarkDecorator: + raise NotImplementedError() + + class _XfailMarkDecorator(MarkDecorator): + @overload # type: ignore[override,misc] + def __call__(self, arg: _Markable) -> _Markable: + raise NotImplementedError() + + @overload # noqa: F811 + def __call__( # noqa: F811 + self, + condition: Union[str, bool] = ..., + *conditions: Union[str, bool], + reason: str = ..., + run: bool = ..., + raises: Union[BaseException, Tuple[BaseException, ...]] = ..., + strict: bool = ... + ) -> MarkDecorator: + raise NotImplementedError() + + class _ParametrizeMarkDecorator(MarkDecorator): + def __call__( # type: ignore[override] + self, + argnames: Union[str, List[str], Tuple[str, ...]], + argvalues: Iterable[Union[ParameterSet, Sequence[object], object]], + *, + indirect: Union[bool, Sequence[str]] = ..., + ids: Optional[ + Union[ + Iterable[Union[None, str, float, int, bool]], + Callable[[object], Optional[object]], + ] + ] = ..., + scope: Optional[_Scope] = ... + ) -> MarkDecorator: + raise NotImplementedError() + + class _UsefixturesMarkDecorator(MarkDecorator): + def __call__( # type: ignore[override] + self, *fixtures: str + ) -> MarkDecorator: + raise NotImplementedError() + + class _FilterwarningsMarkDecorator(MarkDecorator): + def __call__( # type: ignore[override] + self, *filters: str + ) -> MarkDecorator: + raise NotImplementedError() + + class MarkGenerator: """Factory for :class:`MarkDecorator` objects - exposed as a ``pytest.mark`` singleton instance. @@ -397,6 +468,15 @@ def test_function(): _config = None # type: Optional[Config] _markers = set() # type: Set[str] + # See TYPE_CHECKING above. + if TYPE_CHECKING: + skip = None # type: _SkipMarkDecorator + skipif = None # type: _SkipifMarkDecorator + xfail = None # type: _XfailMarkDecorator + parametrize = None # type: _ParametrizeMarkDecorator + usefixtures = None # type: _UsefixturesMarkDecorator + filterwarnings = None # type: _FilterwarningsMarkDecorator + def __getattr__(self, name: str) -> MarkDecorator: if name[0] == "_": raise AttributeError("Marker name must NOT start with underscore") From 4655b7998540d47e6f8dd783c82b37588719556d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 21 Jun 2020 00:34:41 +0300 Subject: [PATCH 402/823] config: improve typing --- src/_pytest/_code/__init__.py | 2 + src/_pytest/_code/code.py | 2 +- src/_pytest/config/__init__.py | 185 ++++++++++++++++++++------------- src/_pytest/helpconfig.py | 6 +- src/_pytest/hookspec.py | 2 +- src/_pytest/logging.py | 12 ++- src/_pytest/pathlib.py | 2 +- src/_pytest/pytester.py | 8 +- src/_pytest/tmpdir.py | 3 +- testing/acceptance_test.py | 4 +- testing/code/test_excinfo.py | 2 +- testing/test_config.py | 6 +- 12 files changed, 143 insertions(+), 91 deletions(-) diff --git a/src/_pytest/_code/__init__.py b/src/_pytest/_code/__init__.py index 38019298c3c..76963c0eb59 100644 --- a/src/_pytest/_code/__init__.py +++ b/src/_pytest/_code/__init__.py @@ -6,6 +6,7 @@ from .code import getfslineno from .code import getrawcode from .code import Traceback +from .code import TracebackEntry from .source import compile_ as compile from .source import Source @@ -17,6 +18,7 @@ "getfslineno", "getrawcode", "Traceback", + "TracebackEntry", "compile", "Source", ] diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 65e5aa6d540..e548bceb76c 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -213,7 +213,7 @@ def statement(self) -> "Source": return source.getstatement(self.lineno) @property - def path(self): + def path(self) -> Union[py.path.local, str]: """ path to the source code """ return self.frame.code.path diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index b4a5a70adc7..ac7afcd56fc 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -15,10 +15,13 @@ from typing import Callable from typing import Dict from typing import IO +from typing import Iterable +from typing import Iterator from typing import List from typing import Optional from typing import Sequence from typing import Set +from typing import TextIO from typing import Tuple from typing import Union @@ -42,6 +45,7 @@ from _pytest.outcomes import fail from _pytest.outcomes import Skipped from _pytest.pathlib import import_path +from _pytest.pathlib import ImportMode from _pytest.pathlib import Path from _pytest.store import Store from _pytest.warning_types import PytestConfigWarning @@ -50,6 +54,7 @@ from typing import Type from _pytest._code.code import _TracebackStyle + from _pytest.terminal import TerminalReporter from .argparsing import Argument @@ -88,18 +93,24 @@ class ExitCode(enum.IntEnum): class ConftestImportFailure(Exception): - def __init__(self, path, excinfo): + def __init__( + self, + path: py.path.local, + excinfo: Tuple["Type[Exception]", Exception, TracebackType], + ) -> None: super().__init__(path, excinfo) self.path = path - self.excinfo = excinfo # type: Tuple[Type[Exception], Exception, TracebackType] + self.excinfo = excinfo - def __str__(self): + def __str__(self) -> str: return "{}: {} (from {})".format( self.excinfo[0].__name__, self.excinfo[1], self.path ) -def filter_traceback_for_conftest_import_failure(entry) -> bool: +def filter_traceback_for_conftest_import_failure( + entry: _pytest._code.TracebackEntry, +) -> bool: """filters tracebacks entries which point to pytest internals or importlib. Make a special case for importlib because we use it to import test modules and conftest files @@ -108,7 +119,10 @@ def filter_traceback_for_conftest_import_failure(entry) -> bool: return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep) -def main(args=None, plugins=None) -> Union[int, ExitCode]: +def main( + args: Optional[List[str]] = None, + plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, +) -> Union[int, ExitCode]: """ return exit code, after performing an in-process test run. :arg args: list of command line arguments. @@ -177,7 +191,7 @@ class cmdline: # compatibility namespace main = staticmethod(main) -def filename_arg(path, optname): +def filename_arg(path: str, optname: str) -> str: """ Argparse type validator for filename arguments. :path: path of filename @@ -188,7 +202,7 @@ def filename_arg(path, optname): return path -def directory_arg(path, optname): +def directory_arg(path: str, optname: str) -> str: """Argparse type validator for directory arguments. :path: path of directory @@ -239,13 +253,16 @@ def directory_arg(path, optname): builtin_plugins.add("pytester") -def get_config(args=None, plugins=None): +def get_config( + args: Optional[List[str]] = None, + plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, +) -> "Config": # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() config = Config( pluginmanager, invocation_params=Config.InvocationParams( - args=args or (), plugins=plugins, dir=Path.cwd() + args=args or (), plugins=plugins, dir=Path.cwd(), ), ) @@ -255,10 +272,11 @@ def get_config(args=None, plugins=None): for spec in default_plugins: pluginmanager.import_plugin(spec) + return config -def get_plugin_manager(): +def get_plugin_manager() -> "PytestPluginManager": """ Obtain a new instance of the :py:class:`_pytest.config.PytestPluginManager`, with default plugins @@ -271,8 +289,9 @@ def get_plugin_manager(): def _prepareconfig( - args: Optional[Union[py.path.local, List[str]]] = None, plugins=None -): + args: Optional[Union[py.path.local, List[str]]] = None, + plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, +) -> "Config": if args is None: args = sys.argv[1:] elif isinstance(args, py.path.local): @@ -290,9 +309,10 @@ def _prepareconfig( pluginmanager.consider_pluginarg(plugin) else: pluginmanager.register(plugin) - return pluginmanager.hook.pytest_cmdline_parse( + config = pluginmanager.hook.pytest_cmdline_parse( pluginmanager=pluginmanager, args=args ) + return config except BaseException: config._ensure_unconfigure() raise @@ -313,13 +333,11 @@ def __init__(self) -> None: super().__init__("pytest") # The objects are module objects, only used generically. - self._conftest_plugins = set() # type: Set[object] + self._conftest_plugins = set() # type: Set[types.ModuleType] - # state related to local conftest plugins - # Maps a py.path.local to a list of module objects. - self._dirpath2confmods = {} # type: Dict[Any, List[object]] - # Maps a py.path.local to a module object. - self._conftestpath2mod = {} # type: Dict[Any, object] + # State related to local conftest plugins. + self._dirpath2confmods = {} # type: Dict[py.path.local, List[types.ModuleType]] + self._conftestpath2mod = {} # type: Dict[Path, types.ModuleType] self._confcutdir = None # type: Optional[py.path.local] self._noconftest = False self._duplicatepaths = set() # type: Set[py.path.local] @@ -328,7 +346,7 @@ def __init__(self) -> None: self.register(self) if os.environ.get("PYTEST_DEBUG"): err = sys.stderr # type: IO[str] - encoding = getattr(err, "encoding", "utf8") + encoding = getattr(err, "encoding", "utf8") # type: str try: err = open( os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding, @@ -343,7 +361,7 @@ def __init__(self) -> None: # Used to know when we are importing conftests after the pytest_configure stage self._configured = False - def parse_hookimpl_opts(self, plugin, name): + def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str): # pytest hooks are always prefixed with pytest_ # so we avoid accessing possibly non-readable attributes # (see issue #1073) @@ -372,7 +390,7 @@ def parse_hookimpl_opts(self, plugin, name): opts.setdefault(name, hasattr(method, name) or name in known_marks) return opts - def parse_hookspec_opts(self, module_or_class, name): + def parse_hookspec_opts(self, module_or_class, name: str): opts = super().parse_hookspec_opts(module_or_class, name) if opts is None: method = getattr(module_or_class, name) @@ -389,7 +407,9 @@ def parse_hookspec_opts(self, module_or_class, name): } return opts - def register(self, plugin: _PluggyPlugin, name: Optional[str] = None): + def register( + self, plugin: _PluggyPlugin, name: Optional[str] = None + ) -> Optional[str]: if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: warnings.warn( PytestConfigWarning( @@ -399,8 +419,8 @@ def register(self, plugin: _PluggyPlugin, name: Optional[str] = None): ) ) ) - return - ret = super().register(plugin, name) + return None + ret = super().register(plugin, name) # type: Optional[str] if ret: self.hook.pytest_plugin_registered.call_historic( kwargs=dict(plugin=plugin, manager=self) @@ -410,11 +430,12 @@ def register(self, plugin: _PluggyPlugin, name: Optional[str] = None): self.consider_module(plugin) return ret - def getplugin(self, name): + def getplugin(self, name: str): # support deprecated naming because plugins (xdist e.g.) use it - return self.get_plugin(name) + plugin = self.get_plugin(name) # type: Optional[_PluggyPlugin] + return plugin - def hasplugin(self, name): + def hasplugin(self, name: str) -> bool: """Return True if the plugin with the given name is registered.""" return bool(self.get_plugin(name)) @@ -436,7 +457,7 @@ def pytest_configure(self, config: "Config") -> None: # # internal API for local conftest plugin handling # - def _set_initial_conftests(self, namespace): + def _set_initial_conftests(self, namespace: argparse.Namespace) -> None: """ load initial conftest files given a preparsed "namespace". As conftest files may add their own command line options which have arguments ('--my-opt somepath') we might get some @@ -454,8 +475,8 @@ def _set_initial_conftests(self, namespace): self._using_pyargs = namespace.pyargs testpaths = namespace.file_or_dir foundanchor = False - for path in testpaths: - path = str(path) + for testpath in testpaths: + path = str(testpath) # remove node-id syntax i = path.find("::") if i != -1: @@ -467,7 +488,9 @@ def _set_initial_conftests(self, namespace): if not foundanchor: self._try_load_conftest(current, namespace.importmode) - def _try_load_conftest(self, anchor, importmode): + def _try_load_conftest( + self, anchor: py.path.local, importmode: Union[str, ImportMode] + ) -> None: self._getconftestmodules(anchor, importmode) # let's also consider test* subdirs if anchor.check(dir=1): @@ -476,7 +499,9 @@ def _try_load_conftest(self, anchor, importmode): self._getconftestmodules(x, importmode) @lru_cache(maxsize=128) - def _getconftestmodules(self, path, importmode): + def _getconftestmodules( + self, path: py.path.local, importmode: Union[str, ImportMode], + ) -> List[types.ModuleType]: if self._noconftest: return [] @@ -499,7 +524,9 @@ def _getconftestmodules(self, path, importmode): self._dirpath2confmods[directory] = clist return clist - def _rget_with_confmod(self, name, path, importmode): + def _rget_with_confmod( + self, name: str, path: py.path.local, importmode: Union[str, ImportMode], + ) -> Tuple[types.ModuleType, Any]: modules = self._getconftestmodules(path, importmode) for mod in reversed(modules): try: @@ -508,7 +535,9 @@ def _rget_with_confmod(self, name, path, importmode): continue raise KeyError(name) - def _importconftest(self, conftestpath, importmode): + def _importconftest( + self, conftestpath: py.path.local, importmode: Union[str, ImportMode], + ) -> types.ModuleType: # Use a resolved Path object as key to avoid loading the same conftest twice # with build systems that create build directories containing # symlinks to actual files. @@ -526,7 +555,9 @@ def _importconftest(self, conftestpath, importmode): try: mod = import_path(conftestpath, mode=importmode) except Exception as e: - raise ConftestImportFailure(conftestpath, sys.exc_info()) from e + assert e.__traceback__ is not None + exc_info = (type(e), e, e.__traceback__) + raise ConftestImportFailure(conftestpath, exc_info) from e self._check_non_top_pytest_plugins(mod, conftestpath) @@ -542,7 +573,9 @@ def _importconftest(self, conftestpath, importmode): self.consider_conftest(mod) return mod - def _check_non_top_pytest_plugins(self, mod, conftestpath): + def _check_non_top_pytest_plugins( + self, mod: types.ModuleType, conftestpath: py.path.local, + ) -> None: if ( hasattr(mod, "pytest_plugins") and self._configured @@ -564,7 +597,9 @@ def _check_non_top_pytest_plugins(self, mod, conftestpath): # # - def consider_preparse(self, args, *, exclude_only: bool = False) -> None: + def consider_preparse( + self, args: Sequence[str], *, exclude_only: bool = False + ) -> None: i = 0 n = len(args) while i < n: @@ -585,7 +620,7 @@ def consider_preparse(self, args, *, exclude_only: bool = False) -> None: continue self.consider_pluginarg(parg) - def consider_pluginarg(self, arg) -> None: + def consider_pluginarg(self, arg: str) -> None: if arg.startswith("no:"): name = arg[3:] if name in essential_plugins: @@ -610,7 +645,7 @@ def consider_pluginarg(self, arg) -> None: del self._name2plugin["pytest_" + name] self.import_plugin(arg, consider_entry_points=True) - def consider_conftest(self, conftestmodule) -> None: + def consider_conftest(self, conftestmodule: types.ModuleType) -> None: self.register(conftestmodule, name=conftestmodule.__file__) def consider_env(self) -> None: @@ -619,7 +654,7 @@ def consider_env(self) -> None: def consider_module(self, mod: types.ModuleType) -> None: self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) - def _import_plugin_specs(self, spec): + def _import_plugin_specs(self, spec) -> None: plugins = _get_plugin_specs_as_list(spec) for import_spec in plugins: self.import_plugin(import_spec) @@ -636,7 +671,6 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No assert isinstance(modname, str), ( "module name as text required, got %r" % modname ) - modname = str(modname) if self.is_blocked(modname) or self.get_plugin(modname) is not None: return @@ -668,7 +702,7 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No self.register(mod, modname) -def _get_plugin_specs_as_list(specs): +def _get_plugin_specs_as_list(specs) -> List[str]: """ Parses a list of "plugin specs" and returns a list of plugin names. @@ -688,7 +722,7 @@ def _get_plugin_specs_as_list(specs): return [] -def _ensure_removed_sysmodule(modname): +def _ensure_removed_sysmodule(modname: str) -> None: try: del sys.modules[modname] except KeyError: @@ -703,7 +737,7 @@ def __repr__(self): notset = Notset() -def _iter_rewritable_modules(package_files): +def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: """ Given an iterable of file names in a source distribution, return the "names" that should be marked for assertion rewrite (for example the package "pytest_mock/__init__.py" should @@ -766,6 +800,10 @@ def _iter_rewritable_modules(package_files): yield from _iter_rewritable_modules(new_package_files) +def _args_converter(args: Iterable[str]) -> Tuple[str, ...]: + return tuple(args) + + class Config: """ Access to configuration values, pluginmanager and plugin hooks. @@ -793,9 +831,9 @@ class InvocationParams: Plugins accessing ``InvocationParams`` must be aware of that. """ - args = attr.ib(converter=tuple) + args = attr.ib(type=Tuple[str, ...], converter=_args_converter) """tuple of command-line arguments as passed to ``pytest.main()``.""" - plugins = attr.ib() + plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]]) """list of extra plugins, might be `None`.""" dir = attr.ib(type=Path) """directory where ``pytest.main()`` was invoked from.""" @@ -855,7 +893,7 @@ def invocation_dir(self) -> py.path.local: """Backward compatibility""" return py.path.local(str(self.invocation_params.dir)) - def add_cleanup(self, func) -> None: + def add_cleanup(self, func: Callable[[], None]) -> None: """ Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" self._cleanup.append(func) @@ -876,12 +914,15 @@ def _ensure_unconfigure(self) -> None: fin = self._cleanup.pop() fin() - def get_terminal_writer(self): - return self.pluginmanager.get_plugin("terminalreporter")._tw + def get_terminal_writer(self) -> TerminalWriter: + terminalreporter = self.pluginmanager.get_plugin( + "terminalreporter" + ) # type: TerminalReporter + return terminalreporter._tw def pytest_cmdline_parse( self, pluginmanager: PytestPluginManager, args: List[str] - ) -> object: + ) -> "Config": try: self.parse(args) except UsageError: @@ -923,7 +964,7 @@ def notify_exception( sys.stderr.write("INTERNALERROR> %s\n" % line) sys.stderr.flush() - def cwd_relative_nodeid(self, nodeid): + def cwd_relative_nodeid(self, nodeid: str) -> str: # nodeid's are relative to the rootpath, compute relative to cwd if self.invocation_dir != self.rootdir: fullpath = self.rootdir.join(nodeid) @@ -931,7 +972,7 @@ def cwd_relative_nodeid(self, nodeid): return nodeid @classmethod - def fromdictargs(cls, option_dict, args): + def fromdictargs(cls, option_dict, args) -> "Config": """ constructor usable for subprocesses. """ config = get_config(args) config.option.__dict__.update(option_dict) @@ -949,7 +990,7 @@ def _processopt(self, opt: "Argument") -> None: setattr(self.option, opt.dest, opt.default) @hookimpl(trylast=True) - def pytest_load_initial_conftests(self, early_config): + def pytest_load_initial_conftests(self, early_config: "Config") -> None: self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) def _initini(self, args: Sequence[str]) -> None: @@ -1078,7 +1119,7 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: raise self._validate_keys() - def _checkversion(self): + def _checkversion(self) -> None: import pytest minver = self.inicfg.get("minversion", None) @@ -1167,7 +1208,7 @@ def parse(self, args: List[str], addopts: bool = True) -> None: except PrintHelp: pass - def addinivalue_line(self, name, line): + def addinivalue_line(self, name: str, line: str) -> None: """ add a line to an ini-file option. The option must have been declared but might not yet be set in which case the line becomes the the first line in its value. """ @@ -1186,7 +1227,7 @@ def getini(self, name: str): self._inicache[name] = val = self._getini(name) return val - def _getini(self, name: str) -> Any: + def _getini(self, name: str): try: description, type, default = self._parser._inidict[name] except KeyError as e: @@ -1231,12 +1272,14 @@ def _getini(self, name: str) -> Any: else: return value elif type == "bool": - return bool(_strtobool(str(value).strip())) + return _strtobool(str(value).strip()) else: assert type is None return value - def _getconftest_pathlist(self, name, path): + def _getconftest_pathlist( + self, name: str, path: py.path.local + ) -> Optional[List[py.path.local]]: try: mod, relroots = self.pluginmanager._rget_with_confmod( name, path, self.getoption("importmode") @@ -1244,7 +1287,7 @@ def _getconftest_pathlist(self, name, path): except KeyError: return None modpath = py.path.local(mod.__file__).dirpath() - values = [] + values = [] # type: List[py.path.local] for relroot in relroots: if not isinstance(relroot, py.path.local): relroot = relroot.replace("/", py.path.local.sep) @@ -1295,16 +1338,16 @@ def getoption(self, name: str, default=notset, skip: bool = False): pytest.skip("no {!r} option found".format(name)) raise ValueError("no option named {!r}".format(name)) from e - def getvalue(self, name, path=None): + def getvalue(self, name: str, path=None): """ (deprecated, use getoption()) """ return self.getoption(name) - def getvalueorskip(self, name, path=None): + def getvalueorskip(self, name: str, path=None): """ (deprecated, use getoption(skip=True)) """ return self.getoption(name, skip=True) -def _assertion_supported(): +def _assertion_supported() -> bool: try: assert False except AssertionError: @@ -1313,7 +1356,7 @@ def _assertion_supported(): return False -def _warn_about_missing_assertion(mode): +def _warn_about_missing_assertion(mode) -> None: if not _assertion_supported(): if mode == "plain": sys.stderr.write( @@ -1331,12 +1374,14 @@ def _warn_about_missing_assertion(mode): ) -def create_terminal_writer(config: Config, *args, **kwargs) -> TerminalWriter: +def create_terminal_writer( + config: Config, file: Optional[TextIO] = None +) -> TerminalWriter: """Create a TerminalWriter instance configured according to the options in the config object. Every code which requires a TerminalWriter object and has access to a config object should use this function. """ - tw = TerminalWriter(*args, **kwargs) + tw = TerminalWriter(file=file) if config.option.color == "yes": tw.hasmarkup = True if config.option.color == "no": @@ -1344,8 +1389,8 @@ def create_terminal_writer(config: Config, *args, **kwargs) -> TerminalWriter: return tw -def _strtobool(val): - """Convert a string representation of truth to true (1) or false (0). +def _strtobool(val: str) -> bool: + """Convert a string representation of truth to True or False. True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if @@ -1355,8 +1400,8 @@ def _strtobool(val): """ val = val.lower() if val in ("y", "yes", "t", "true", "on", "1"): - return 1 + return True elif val in ("n", "no", "f", "false", "off", "0"): - return 0 + return False else: raise ValueError("invalid truth value {!r}".format(val)) diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 06e0954cf08..24952852be4 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -96,7 +96,7 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(hookwrapper=True) def pytest_cmdline_parse(): outcome = yield - config = outcome.get_result() + config = outcome.get_result() # type: Config if config.option.debug: path = os.path.abspath("pytestdebug.log") debugfile = open(path, "w") @@ -124,7 +124,7 @@ def unset_tracing() -> None: config.add_cleanup(unset_tracing) -def showversion(config): +def showversion(config: Config) -> None: if config.option.version > 1: sys.stderr.write( "This is pytest version {}, imported from {}\n".format( @@ -224,7 +224,7 @@ def showhelp(config: Config) -> None: conftest_options = [("pytest_plugins", "list of plugin names to load")] -def getpluginversioninfo(config): +def getpluginversioninfo(config: Config) -> List[str]: lines = [] plugininfo = config.pluginmanager.list_plugin_distinfo() if plugininfo: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index eba6f5ba9f2..c05b60791d3 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -143,7 +143,7 @@ def pytest_configure(config: "Config") -> None: @hookspec(firstresult=True) def pytest_cmdline_parse( pluginmanager: "PytestPluginManager", args: List[str] -) -> Optional[object]: +) -> Optional["Config"]: """return initialized config object, parsing the specified args. Stops at first non-None result, see :ref:`firstresult` diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index a06dc1ab553..57aa14f2774 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -141,9 +141,14 @@ def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int: if auto_indent_option is None: return 0 - elif type(auto_indent_option) is int: + elif isinstance(auto_indent_option, bool): + if auto_indent_option: + return -1 + else: + return 0 + elif isinstance(auto_indent_option, int): return int(auto_indent_option) - elif type(auto_indent_option) is str: + elif isinstance(auto_indent_option, str): try: return int(auto_indent_option) except ValueError: @@ -153,9 +158,6 @@ def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int: return -1 except ValueError: return 0 - elif type(auto_indent_option) is bool: - if auto_indent_option: - return -1 return 0 diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 66ae9a51dd1..dd7443f07e3 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -466,7 +466,7 @@ def import_path( """ mode = ImportMode(mode) - path = Path(p) + path = Path(str(p)) if not path.exists(): raise ImportError(path) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index cf3dbd2011d..fd4c10577a8 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1054,7 +1054,7 @@ def _ensure_basetemp(self, args): args.append("--basetemp=%s" % self.tmpdir.dirpath("basetemp")) return args - def parseconfig(self, *args: Union[str, py.path.local]) -> Config: + def parseconfig(self, *args) -> Config: """Return a new pytest Config instance from given commandline args. This invokes the pytest bootstrapping code in _pytest.config to create @@ -1070,14 +1070,14 @@ def parseconfig(self, *args: Union[str, py.path.local]) -> Config: import _pytest.config - config = _pytest.config._prepareconfig(args, self.plugins) # type: Config + config = _pytest.config._prepareconfig(args, self.plugins) # type: ignore[arg-type] # we don't know what the test will do with this half-setup config # object and thus we make sure it gets unconfigured properly in any # case (otherwise capturing could still be active, for example) self.request.addfinalizer(config._ensure_unconfigure) return config - def parseconfigure(self, *args): + def parseconfigure(self, *args) -> Config: """Return a new pytest configured Config instance. This returns a new :py:class:`_pytest.config.Config` instance like @@ -1318,7 +1318,7 @@ def runpytest_subprocess(self, *args, timeout=None) -> RunResult: Returns a :py:class:`RunResult`. """ __tracebackhide__ = True - p = make_numbered_dir(root=Path(self.tmpdir), prefix="runpytest-") + p = make_numbered_dir(root=Path(str(self.tmpdir)), prefix="runpytest-") args = ("--basetemp=%s" % p,) + args plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 199c7c9374e..f6d1799ad6f 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -13,6 +13,7 @@ from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup from .pathlib import Path +from _pytest.config import Config from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch @@ -135,7 +136,7 @@ def get_user() -> Optional[str]: return None -def pytest_configure(config) -> None: +def pytest_configure(config: Config) -> None: """Create a TempdirFactory and attach it to the config object. This is to comply with existing plugins which expect the handler to be diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index d8f7a501a01..66c2bf0bfd4 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -585,11 +585,11 @@ def test_equivalence_pytest_pydottest(self) -> None: # Type ignored because `py.test` is not and will not be typed. assert pytest.main == py.test.cmdline.main # type: ignore[attr-defined] - def test_invoke_with_invalid_type(self): + def test_invoke_with_invalid_type(self) -> None: with pytest.raises( TypeError, match="expected to be a list of strings, got: '-h'" ): - pytest.main("-h") + pytest.main("-h") # type: ignore[arg-type] def test_invoke_with_path(self, tmpdir, capsys): retcode = pytest.main(tmpdir) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 0ff00bcaa8b..75c937612e9 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -372,7 +372,7 @@ def test_excinfo_no_python_sourcecode(tmpdir): for item in excinfo.traceback: print(item) # XXX: for some reason jinja.Template.render is printed in full item.source # shouldn't fail - if item.path.basename == "test.txt": + if isinstance(item.path, py.path.local) and item.path.basename == "test.txt": assert str(item.source) == "{{ h()}}:" diff --git a/testing/test_config.py b/testing/test_config.py index 4e64a6928a8..c1e4471b987 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1778,5 +1778,7 @@ def test_conftest_import_error_repr(tmpdir): ): try: raise RuntimeError("some error") - except Exception as e: - raise ConftestImportFailure(path, sys.exc_info()) from e + except Exception as exc: + assert exc.__traceback__ is not None + exc_info = (type(exc), exc, exc.__traceback__) + raise ConftestImportFailure(path, exc_info) from exc From 04a6d378234e3c72055c7e90084b1a2d36d3f89d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 22 Jun 2020 15:07:50 +0300 Subject: [PATCH 403/823] nodes: fix string possibly stored in Node.keywords instead of MarkDecorator This mistake was introduced in 7259c453d6c1dba6727cd328e6db5635ccf5821c. --- src/_pytest/nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 4c7aa1bcd2c..24e4665865b 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -276,7 +276,7 @@ def add_marker( marker_ = getattr(MARK_GEN, marker) else: raise ValueError("is not a string or pytest.mark.* Marker") - self.keywords[marker_.name] = marker + self.keywords[marker_.name] = marker_ if append: self.own_markers.append(marker_.mark) else: From 8994e1e3a17bd625e0c258d0a402062542908fe3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 23 Jun 2020 11:38:21 +0300 Subject: [PATCH 404/823] config: make _get_plugin_specs_as_list a little clearer and more general --- src/_pytest/config/__init__.py | 41 +++++++++++++++++++--------------- testing/test_config.py | 17 ++++++-------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ac7afcd56fc..717743e7946 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1,5 +1,6 @@ """ command line options, ini-file and conftest.py processing. """ import argparse +import collections.abc import contextlib import copy import enum @@ -654,7 +655,9 @@ def consider_env(self) -> None: def consider_module(self, mod: types.ModuleType) -> None: self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) - def _import_plugin_specs(self, spec) -> None: + def _import_plugin_specs( + self, spec: Union[None, types.ModuleType, str, Sequence[str]] + ) -> None: plugins = _get_plugin_specs_as_list(spec) for import_spec in plugins: self.import_plugin(import_spec) @@ -702,24 +705,26 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No self.register(mod, modname) -def _get_plugin_specs_as_list(specs) -> List[str]: - """ - Parses a list of "plugin specs" and returns a list of plugin names. - - Plugin specs can be given as a list of strings separated by "," or already as a list/tuple in - which case it is returned as a list. Specs can also be `None` in which case an - empty list is returned. - """ - if specs is not None and not isinstance(specs, types.ModuleType): - if isinstance(specs, str): - specs = specs.split(",") if specs else [] - if not isinstance(specs, (list, tuple)): - raise UsageError( - "Plugin specs must be a ','-separated string or a " - "list/tuple of strings for plugin names. Given: %r" % specs - ) +def _get_plugin_specs_as_list( + specs: Union[None, types.ModuleType, str, Sequence[str]] +) -> List[str]: + """Parse a plugins specification into a list of plugin names.""" + # None means empty. + if specs is None: + return [] + # Workaround for #3899 - a submodule which happens to be called "pytest_plugins". + if isinstance(specs, types.ModuleType): + return [] + # Comma-separated list. + if isinstance(specs, str): + return specs.split(",") if specs else [] + # Direct specification. + if isinstance(specs, collections.abc.Sequence): return list(specs) - return [] + raise UsageError( + "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %r" + % specs + ) def _ensure_removed_sysmodule(modname: str) -> None: diff --git a/testing/test_config.py b/testing/test_config.py index c1e4471b987..bc0da93a57b 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -11,6 +11,7 @@ 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 Config from _pytest.config import ConftestImportFailure @@ -1115,21 +1116,17 @@ def pytest_load_initial_conftests(self): assert [x.function.__module__ for x in values] == expected -def test_get_plugin_specs_as_list(): - from _pytest.config import _get_plugin_specs_as_list - - def exp_match(val): +def test_get_plugin_specs_as_list() -> None: + def exp_match(val: object) -> str: return ( - "Plugin specs must be a ','-separated string" - " or a list/tuple of strings for plugin names. Given: {}".format( - re.escape(repr(val)) - ) + "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %s" + % re.escape(repr(val)) ) with pytest.raises(pytest.UsageError, match=exp_match({"foo"})): - _get_plugin_specs_as_list({"foo"}) + _get_plugin_specs_as_list({"foo"}) # type: ignore[arg-type] with pytest.raises(pytest.UsageError, match=exp_match({})): - _get_plugin_specs_as_list(dict()) + _get_plugin_specs_as_list(dict()) # type: ignore[arg-type] assert _get_plugin_specs_as_list(None) == [] assert _get_plugin_specs_as_list("") == [] From 617bf8be5b0d5fa59dfb72a27c66f4f5f54f7e26 Mon Sep 17 00:00:00 2001 From: David Diaz Barquero Date: Tue, 23 Jun 2020 10:03:46 -0600 Subject: [PATCH 405/823] Add details to error message for junit (#7390) Co-authored-by: Bruno Oliveira --- changelog/7385.improvement.rst | 13 +++++++++++++ src/_pytest/junitxml.py | 10 ++++++++-- testing/test_junitxml.py | 12 +++++++----- 3 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 changelog/7385.improvement.rst diff --git a/changelog/7385.improvement.rst b/changelog/7385.improvement.rst new file mode 100644 index 00000000000..c02fee5dad1 --- /dev/null +++ b/changelog/7385.improvement.rst @@ -0,0 +1,13 @@ +``--junitxml`` now includes the exception cause in the ``message`` XML attribute for failures during setup and teardown. + +Previously: + +.. code-block:: xml + + + +Now: + +.. code-block:: xml + + diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 86e8fcf3810..4df7535de8e 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -236,10 +236,16 @@ def append_collect_skipped(self, report: TestReport) -> None: self._add_simple(Junit.skipped, "collection skipped", report.longrepr) def append_error(self, report: TestReport) -> None: + assert report.longrepr is not None + if getattr(report.longrepr, "reprcrash", None) is not None: + reason = report.longrepr.reprcrash.message + else: + reason = str(report.longrepr) + if report.when == "teardown": - msg = "test teardown failure" + msg = 'failed on teardown with "{}"'.format(reason) else: - msg = "test setup failure" + msg = 'failed on setup with "{}"'.format(reason) self._add_simple(Junit.error, msg, report.longrepr) def append_skipped(self, report: TestReport) -> None: diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index f8a6a295f6b..5e5826b236a 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -266,7 +266,7 @@ def test_setup_error(self, testdir, run_and_parse, xunit_family): @pytest.fixture def arg(request): - raise ValueError() + raise ValueError("Error reason") def test_function(arg): pass """ @@ -278,7 +278,7 @@ def test_function(arg): tnode = node.find_first_by_tag("testcase") tnode.assert_attr(classname="test_setup_error", name="test_function") fnode = tnode.find_first_by_tag("error") - fnode.assert_attr(message="test setup failure") + fnode.assert_attr(message='failed on setup with "ValueError: Error reason"') assert "ValueError" in fnode.toxml() @parametrize_families @@ -290,7 +290,7 @@ def test_teardown_error(self, testdir, run_and_parse, xunit_family): @pytest.fixture def arg(): yield - raise ValueError() + raise ValueError('Error reason') def test_function(arg): pass """ @@ -301,7 +301,7 @@ def test_function(arg): tnode = node.find_first_by_tag("testcase") tnode.assert_attr(classname="test_teardown_error", name="test_function") fnode = tnode.find_first_by_tag("error") - fnode.assert_attr(message="test teardown failure") + fnode.assert_attr(message='failed on teardown with "ValueError: Error reason"') assert "ValueError" in fnode.toxml() @parametrize_families @@ -328,7 +328,9 @@ def test_function(arg): fnode = first.find_first_by_tag("failure") fnode.assert_attr(message="Exception: Call Exception") snode = second.find_first_by_tag("error") - snode.assert_attr(message="test teardown failure") + snode.assert_attr( + message='failed on teardown with "Exception: Teardown Exception"' + ) @parametrize_families def test_skip_contains_name_reason(self, testdir, run_and_parse, xunit_family): From 6cbbd2d90b6c3cd964df214bbcd7212b3450e74d Mon Sep 17 00:00:00 2001 From: Daniel <61800298+ffe4@users.noreply.github.com> Date: Tue, 23 Jun 2020 22:38:11 +0200 Subject: [PATCH 406/823] Fix typo in examples/markers.rst --- doc/en/example/markers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index e791f489d0d..1fd10101c0e 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -639,7 +639,7 @@ Automatically adding markers based on test names .. regendoc:wipe -If you a test suite where test function names indicate a certain +If you have a test suite where test function names indicate a certain type of test, you can implement a hook that automatically defines markers so that you can use the ``-m`` option with it. Let's look at this test module: From 474973afa401ea8bf177d89025022d5ea3801c4d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 24 Jun 2020 15:42:07 +0300 Subject: [PATCH 407/823] CONTRIBUTING: sync changelog types The got out of date with the actual ones we use. --- CONTRIBUTING.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5e309a31728..9ff854ffaf7 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -173,8 +173,10 @@ Short version The test environments above are usually enough to cover most cases locally. #. Write a ``changelog`` entry: ``changelog/2574.bugfix.rst``, use issue id number - and one of ``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or - ``trivial`` for the issue type. + and one of ``feature``, ``improvement``, ``bugfix``, ``doc``, ``deprecation``, + ``breaking``, ``vendor`` or ``trivial`` for the issue type. + + #. Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please add yourself to the ``AUTHORS`` file, in alphabetical order. @@ -274,8 +276,9 @@ Here is a simple overview, with pytest-specific bits: #. Create a new changelog entry in ``changelog``. The file should be named ``..rst``, where *issueid* is the number of the issue related to the change and *type* is one of - ``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or ``trivial``. You may not create a - changelog entry if the change doesn't affect the documented behaviour of Pytest. + ``feature``, ``improvement``, ``bugfix``, ``doc``, ``deprecation``, ``breaking``, ``vendor`` + or ``trivial``. You may skip creating the changelog entry if the change doesn't affect the + documented behaviour of pytest. #. Add yourself to ``AUTHORS`` file if not there yet, in alphabetical order. From f00bec2a12a585eee245284c8eac86edc27e661f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 25 Jun 2020 14:05:46 +0300 Subject: [PATCH 408/823] Replace yield_fixture -> fixture in internal code `yield_fixture` is a deprecated alias to `fixture`. --- src/_pytest/fixtures.py | 4 +--- src/_pytest/recwarn.py | 4 ++-- testing/example_scripts/issue_519.py | 4 ++-- testing/python/fixtures.py | 5 ++--- testing/test_doctest.py | 2 +- testing/test_pathlib.py | 2 +- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 4b2c6a7742c..9423df7e4c3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -924,9 +924,7 @@ def _teardown_yield_fixture(fixturefunc, it) -> None: except StopIteration: pass else: - fail_fixturefunc( - fixturefunc, "yield_fixture function has more than one 'yield'" - ) + fail_fixturefunc(fixturefunc, "fixture function has more than one 'yield'") def _eval_scope_callable( diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 57034be2aca..eed79c3fdc7 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -13,14 +13,14 @@ from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING -from _pytest.fixtures import yield_fixture +from _pytest.fixtures import fixture from _pytest.outcomes import fail if TYPE_CHECKING: from typing import Type -@yield_fixture +@fixture def recwarn(): """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. diff --git a/testing/example_scripts/issue_519.py b/testing/example_scripts/issue_519.py index 52d5d3f55b1..021dada4923 100644 --- a/testing/example_scripts/issue_519.py +++ b/testing/example_scripts/issue_519.py @@ -33,13 +33,13 @@ def checked_order(): ] -@pytest.yield_fixture(scope="module") +@pytest.fixture(scope="module") def fix1(request, arg1, checked_order): checked_order.append((request.node.name, "fix1", arg1)) yield "fix1-" + arg1 -@pytest.yield_fixture(scope="function") +@pytest.fixture(scope="function") def fix2(request, fix1, arg2, checked_order): checked_order.append((request.node.name, "fix2", arg2)) yield "fix2-" + arg2 + fix1 diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index e14385144aa..3efbbe10757 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1315,7 +1315,7 @@ def test_setup_functions_as_fixtures(self, testdir): DB_INITIALIZED = None - @pytest.yield_fixture(scope="session", autouse=True) + @pytest.fixture(scope="session", autouse=True) def db(): global DB_INITIALIZED DB_INITIALIZED = True @@ -2960,8 +2960,7 @@ def test_params_and_ids_yieldfixture(self, testdir): """ import pytest - @pytest.yield_fixture(params=[object(), object()], - ids=['alpha', 'beta']) + @pytest.fixture(params=[object(), object()], ids=['alpha', 'beta']) def fix(request): yield request.param diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 2b98b5267da..9ef9417cd7e 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1176,7 +1176,7 @@ def test_doctest_module_session_fixture(self, testdir): import pytest import sys - @pytest.yield_fixture(autouse=True, scope='session') + @pytest.fixture(autouse=True, scope='session') def myfixture(): assert not hasattr(sys, 'pytest_session_data') sys.pytest_session_data = 1 diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 126e1718e7a..d9d3894f935 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -91,7 +91,7 @@ class TestImportPath: Having our own pyimport-like function is inline with removing py.path dependency in the future. """ - @pytest.yield_fixture(scope="session") + @pytest.fixture(scope="session") def path1(self, tmpdir_factory): path = tmpdir_factory.mktemp("path") self.setuptestfs(path) From 4d813fdf5e258c634a428d5a8b14e3f4364f4bc1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 25 Jun 2020 14:08:47 +0300 Subject: [PATCH 409/823] recwarn: improve return type annotation of non-contextmanager pytest.warns It returns the return value of the function. --- src/_pytest/recwarn.py | 11 +++++++---- testing/test_recwarn.py | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index eed79c3fdc7..13622e95d46 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -9,6 +9,7 @@ from typing import Optional from typing import Pattern from typing import Tuple +from typing import TypeVar from typing import Union from _pytest.compat import overload @@ -20,6 +21,9 @@ from typing import Type +T = TypeVar("T") + + @fixture def recwarn(): """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. @@ -67,11 +71,10 @@ def warns( @overload # noqa: F811 def warns( # noqa: F811 expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]], - func: Callable, + func: Callable[..., T], *args: Any, - match: Optional[Union[str, "Pattern"]] = ..., **kwargs: Any -) -> Union[Any]: +) -> T: raise NotImplementedError() @@ -97,7 +100,7 @@ def warns( # noqa: F811 ... warnings.warn("my warning", RuntimeWarning) In the context manager form you may use the keyword argument ``match`` to assert - that the exception matches a text or regex:: + that the warning matches a text or regex:: >>> with warns(UserWarning, match='must be 0 or None'): ... warnings.warn("value must be 0 or None", UserWarning) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 1d445d1bf05..f61f8586f9c 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -370,13 +370,14 @@ def test_none_of_multiple_warns(self) -> None: @pytest.mark.filterwarnings("ignore") def test_can_capture_previously_warned(self) -> None: - def f(): + def f() -> int: warnings.warn(UserWarning("ohai")) return 10 assert f() == 10 assert pytest.warns(UserWarning, f) == 10 assert pytest.warns(UserWarning, f) == 10 + assert pytest.warns(UserWarning, f) != "10" # type: ignore[comparison-overlap] def test_warns_context_manager_with_kwargs(self) -> None: with pytest.raises(TypeError) as excinfo: From 653c83e127ab4826de456d796ac98b6aebee2f39 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 25 Jun 2020 14:13:41 +0300 Subject: [PATCH 410/823] recwarn: type annotate recwarn fixture --- src/_pytest/recwarn.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 13622e95d46..3a75e21a398 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -4,6 +4,7 @@ from types import TracebackType from typing import Any from typing import Callable +from typing import Generator from typing import Iterator from typing import List from typing import Optional @@ -25,7 +26,7 @@ @fixture -def recwarn(): +def recwarn() -> Generator["WarningsRecorder", None, None]: """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. See http://docs.python.org/library/warnings.html for information From 142d8963e6e24990dba28e1278fd0db15fe6e832 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 25 Jun 2020 14:30:24 +0300 Subject: [PATCH 411/823] recwarn: type annotate pytest.deprecated_call Also improve its documentation. --- src/_pytest/recwarn.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 3a75e21a398..49bb909ccfc 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -38,9 +38,26 @@ def recwarn() -> Generator["WarningsRecorder", None, None]: yield wrec -def deprecated_call(func=None, *args, **kwargs): - """context manager that can be used to ensure a block of code triggers a - ``DeprecationWarning`` or ``PendingDeprecationWarning``:: +@overload +def deprecated_call( + *, match: Optional[Union[str, "Pattern"]] = ... +) -> "WarningsRecorder": + raise NotImplementedError() + + +@overload # noqa: F811 +def deprecated_call( # noqa: F811 + func: Callable[..., T], *args: Any, **kwargs: Any +) -> T: + raise NotImplementedError() + + +def deprecated_call( # noqa: F811 + func: Optional[Callable] = None, *args: Any, **kwargs: Any +) -> Union["WarningsRecorder", Any]: + """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning``. + + This function can be used as a context manager:: >>> import warnings >>> def api_call_v2(): @@ -50,9 +67,15 @@ def deprecated_call(func=None, *args, **kwargs): >>> with deprecated_call(): ... assert api_call_v2() == 200 - ``deprecated_call`` can also be used by passing a function and ``*args`` and ``*kwargs``, - in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings - types above. + It can also be used by passing a function and ``*args`` and ``**kwargs``, + in which case it will ensure calling ``func(*args, **kwargs)`` produces one of + the warnings types above. The return value is the return value of the function. + + In the context manager form you may use the keyword argument ``match`` to assert + that the warning matches a text or regex. + + The context manager produces a list of :class:`warnings.WarningMessage` objects, + one for each warning raised. """ __tracebackhide__ = True if func is not None: From 8f8f4723790dd035e32309239e0916929a5b0d67 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 25 Jun 2020 15:02:04 +0300 Subject: [PATCH 412/823] python_api: type annotate some parts of pytest.approx() --- src/_pytest/python_api.py | 41 ++++++++++++++++--------------- testing/python/approx.py | 51 ++++++++++++++++++++++----------------- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index c185a06766a..e30471995e7 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -33,7 +33,7 @@ BASE_TYPE = (type, STRING_TYPES) -def _non_numeric_type_error(value, at): +def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: at_str = " at {}".format(at) if at else "" return TypeError( "cannot make approximate comparisons to non-numeric values: {!r} {}".format( @@ -55,7 +55,7 @@ class ApproxBase: __array_ufunc__ = None __array_priority__ = 100 - def __init__(self, expected, rel=None, abs=None, nan_ok=False): + def __init__(self, expected, rel=None, abs=None, nan_ok: bool = False) -> None: __tracebackhide__ = True self.expected = expected self.abs = abs @@ -63,10 +63,10 @@ def __init__(self, expected, rel=None, abs=None, nan_ok=False): self.nan_ok = nan_ok self._check_type() - def __repr__(self): + def __repr__(self) -> str: raise NotImplementedError - def __eq__(self, actual): + def __eq__(self, actual) -> bool: return all( a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual) ) @@ -74,10 +74,10 @@ def __eq__(self, actual): # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore - def __ne__(self, actual): + def __ne__(self, actual) -> bool: return not (actual == self) - def _approx_scalar(self, x): + def _approx_scalar(self, x) -> "ApproxScalar": return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) def _yield_comparisons(self, actual): @@ -87,7 +87,7 @@ def _yield_comparisons(self, actual): """ raise NotImplementedError - def _check_type(self): + def _check_type(self) -> None: """ Raise a TypeError if the expected value is not a valid type. """ @@ -111,11 +111,11 @@ class ApproxNumpy(ApproxBase): Perform approximate comparisons where the expected value is numpy array. """ - def __repr__(self): + def __repr__(self) -> str: list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist()) return "approx({!r})".format(list_scalars) - def __eq__(self, actual): + def __eq__(self, actual) -> bool: import numpy as np # self.expected is supposed to always be an array here @@ -154,12 +154,12 @@ class ApproxMapping(ApproxBase): numeric values (the keys can be anything). """ - def __repr__(self): + def __repr__(self) -> str: return "approx({!r})".format( {k: self._approx_scalar(v) for k, v in self.expected.items()} ) - def __eq__(self, actual): + def __eq__(self, actual) -> bool: if set(actual.keys()) != set(self.expected.keys()): return False @@ -169,7 +169,7 @@ def _yield_comparisons(self, actual): for k in self.expected.keys(): yield actual[k], self.expected[k] - def _check_type(self): + def _check_type(self) -> None: __tracebackhide__ = True for key, value in self.expected.items(): if isinstance(value, type(self.expected)): @@ -185,7 +185,7 @@ class ApproxSequencelike(ApproxBase): numbers. """ - def __repr__(self): + def __repr__(self) -> str: seq_type = type(self.expected) if seq_type not in (tuple, list, set): seq_type = list @@ -193,7 +193,7 @@ def __repr__(self): seq_type(self._approx_scalar(x) for x in self.expected) ) - def __eq__(self, actual): + def __eq__(self, actual) -> bool: if len(actual) != len(self.expected): return False return ApproxBase.__eq__(self, actual) @@ -201,7 +201,7 @@ def __eq__(self, actual): def _yield_comparisons(self, actual): return zip(actual, self.expected) - def _check_type(self): + def _check_type(self) -> None: __tracebackhide__ = True for index, x in enumerate(self.expected): if isinstance(x, type(self.expected)): @@ -223,7 +223,7 @@ class ApproxScalar(ApproxBase): DEFAULT_ABSOLUTE_TOLERANCE = 1e-12 # type: Union[float, Decimal] DEFAULT_RELATIVE_TOLERANCE = 1e-6 # type: Union[float, Decimal] - def __repr__(self): + def __repr__(self) -> str: """ Return a string communicating both the expected value and the tolerance for the comparison being made, e.g. '1.0 ± 1e-6', '(3+4j) ± 5e-6 ∠ ±180°'. @@ -245,7 +245,7 @@ def __repr__(self): return "{} ± {}".format(self.expected, vetted_tolerance) - def __eq__(self, actual): + def __eq__(self, actual) -> bool: """ Return true if the given value is equal to the expected value within the pre-specified tolerance. @@ -275,7 +275,8 @@ def __eq__(self, actual): return False # Return true if the two numbers are within the tolerance. - return abs(self.expected - actual) <= self.tolerance + result = abs(self.expected - actual) <= self.tolerance # type: bool + return result # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore @@ -337,7 +338,7 @@ class ApproxDecimal(ApproxScalar): DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6") -def approx(expected, rel=None, abs=None, nan_ok=False): +def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: """ Assert that two numbers (or two sets of numbers) are equal to each other within some tolerance. @@ -527,7 +528,7 @@ def approx(expected, rel=None, abs=None, nan_ok=False): return cls(expected, rel, abs, nan_ok) -def _is_numpy_array(obj): +def _is_numpy_array(obj: object) -> bool: """ Return true if the given object is a numpy array. Make a special effort to avoid importing numpy unless it's really necessary. diff --git a/testing/python/approx.py b/testing/python/approx.py index 8581475e1ab..db67fe5aa7f 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -3,6 +3,7 @@ from fractions import Fraction from operator import eq from operator import ne +from typing import Optional import pytest from pytest import approx @@ -121,18 +122,22 @@ def test_zero_tolerance(self): assert a == approx(x, rel=5e-1, abs=0.0) assert a != approx(x, rel=5e-2, abs=0.0) - def test_negative_tolerance(self): + @pytest.mark.parametrize( + ("rel", "abs"), + [ + (-1e100, None), + (None, -1e100), + (1e100, -1e100), + (-1e100, 1e100), + (-1e100, -1e100), + ], + ) + def test_negative_tolerance( + self, rel: Optional[float], abs: Optional[float] + ) -> None: # Negative tolerances are not allowed. - illegal_kwargs = [ - dict(rel=-1e100), - dict(abs=-1e100), - dict(rel=1e100, abs=-1e100), - dict(rel=-1e100, abs=1e100), - dict(rel=-1e100, abs=-1e100), - ] - for kwargs in illegal_kwargs: - with pytest.raises(ValueError): - 1.1 == approx(1, **kwargs) + with pytest.raises(ValueError): + 1.1 == approx(1, rel, abs) def test_inf_tolerance(self): # Everything should be equal if the tolerance is infinite. @@ -143,19 +148,21 @@ def test_inf_tolerance(self): assert a == approx(x, rel=0.0, abs=inf) assert a == approx(x, rel=inf, abs=inf) - def test_inf_tolerance_expecting_zero(self): + def test_inf_tolerance_expecting_zero(self) -> None: # If the relative tolerance is zero but the expected value is infinite, # the actual tolerance is a NaN, which should be an error. - illegal_kwargs = [dict(rel=inf, abs=0.0), dict(rel=inf, abs=inf)] - for kwargs in illegal_kwargs: - with pytest.raises(ValueError): - 1 == approx(0, **kwargs) - - def test_nan_tolerance(self): - illegal_kwargs = [dict(rel=nan), dict(abs=nan), dict(rel=nan, abs=nan)] - for kwargs in illegal_kwargs: - with pytest.raises(ValueError): - 1.1 == approx(1, **kwargs) + with pytest.raises(ValueError): + 1 == approx(0, rel=inf, abs=0.0) + with pytest.raises(ValueError): + 1 == approx(0, rel=inf, abs=inf) + + def test_nan_tolerance(self) -> None: + with pytest.raises(ValueError): + 1.1 == approx(1, rel=nan) + with pytest.raises(ValueError): + 1.1 == approx(1, abs=nan) + with pytest.raises(ValueError): + 1.1 == approx(1, rel=nan, abs=nan) def test_reasonable_defaults(self): # Whatever the defaults are, they should work for numbers close to 1 From 97a11726e2bcfdf2fcbcb38c5cb859257bc48f71 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 25 Jun 2020 15:15:08 +0300 Subject: [PATCH 413/823] freeze_support: type annotate --- src/_pytest/freeze_support.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/_pytest/freeze_support.py b/src/_pytest/freeze_support.py index 9d35d9afc60..63c14ecebfa 100644 --- a/src/_pytest/freeze_support.py +++ b/src/_pytest/freeze_support.py @@ -2,9 +2,13 @@ Provides a function to report all internal modules for using freezing tools pytest """ +import types +from typing import Iterator +from typing import List +from typing import Union -def freeze_includes(): +def freeze_includes() -> List[str]: """ Returns a list of module names used by pytest that should be included by cx_freeze. @@ -17,7 +21,9 @@ def freeze_includes(): return result -def _iter_all_modules(package, prefix=""): +def _iter_all_modules( + package: Union[str, types.ModuleType], prefix: str = "", +) -> Iterator[str]: """ Iterates over the names of all modules that can be found in the given package, recursively. @@ -29,10 +35,13 @@ def _iter_all_modules(package, prefix=""): import os import pkgutil - if type(package) is not str: - path, prefix = package.__path__[0], package.__name__ + "." - else: + if isinstance(package, str): path = package + else: + # Type ignored because typeshed doesn't define ModuleType.__path__ + # (only defined on packages). + package_path = package.__path__ # type: ignore[attr-defined] + path, prefix = package_path[0], package.__name__ + "." for _, name, is_package in pkgutil.iter_modules([path]): if is_package: for m in _iter_all_modules(os.path.join(path, name), prefix=name + "."): From 256a5d8b1458bcd0f73b1722423b07c03e450f5b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 25 Jun 2020 15:38:54 +0300 Subject: [PATCH 414/823] hookspec: improve typing of some remaining hooks --- src/_pytest/hookspec.py | 92 +++++++++++++++++++++++------------------ src/_pytest/main.py | 8 ++-- src/_pytest/python.py | 2 +- src/_pytest/reports.py | 36 ++++++++++------ src/_pytest/terminal.py | 6 +-- src/_pytest/unittest.py | 2 +- 6 files changed, 82 insertions(+), 64 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index c05b60791d3..1b4b09c85d4 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,5 +1,6 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ from typing import Any +from typing import Dict from typing import List from typing import Mapping from typing import Optional @@ -37,7 +38,6 @@ from _pytest.python import Metafunc from _pytest.python import Module from _pytest.python import PyCollector - from _pytest.reports import BaseReport from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.runner import CallInfo @@ -172,7 +172,7 @@ def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None: @hookspec(firstresult=True) -def pytest_cmdline_main(config: "Config") -> "Optional[Union[ExitCode, int]]": +def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]: """ called for performing the main command line action. The default implementation will invoke the configure hooks and runtest_mainloop. @@ -206,7 +206,7 @@ def pytest_load_initial_conftests( @hookspec(firstresult=True) -def pytest_collection(session: "Session") -> Optional[Any]: +def pytest_collection(session: "Session") -> Optional[object]: """Perform the collection protocol for the given session. Stops at first non-None result, see :ref:`firstresult`. @@ -242,20 +242,21 @@ def pytest_collection_modifyitems( """ -def pytest_collection_finish(session: "Session"): - """ called after collection has been performed and modified. +def pytest_collection_finish(session: "Session") -> None: + """Called after collection has been performed and modified. :param _pytest.main.Session session: the pytest session object """ @hookspec(firstresult=True) -def pytest_ignore_collect(path, config: "Config"): - """ return True to prevent considering this path for collection. +def pytest_ignore_collect(path: py.path.local, config: "Config") -> Optional[bool]: + """Return True to prevent considering this path for collection. + This hook is consulted for all files and directories prior to calling more specific hooks. - Stops at first non-None result, see :ref:`firstresult` + Stops at first non-None result, see :ref:`firstresult`. :param path: a :py:class:`py.path.local` - the path to analyze :param _pytest.config.Config config: pytest config object @@ -263,18 +264,19 @@ def pytest_ignore_collect(path, config: "Config"): @hookspec(firstresult=True, warn_on_impl=COLLECT_DIRECTORY_HOOK) -def pytest_collect_directory(path, parent): - """ called before traversing a directory for collection files. +def pytest_collect_directory(path: py.path.local, parent) -> Optional[object]: + """Called before traversing a directory for collection files. - Stops at first non-None result, see :ref:`firstresult` + Stops at first non-None result, see :ref:`firstresult`. :param path: a :py:class:`py.path.local` - the path to analyze """ def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]": - """ return collection Node or None for the given path. Any new node - needs to have the specified ``parent`` as a parent. + """Return collection Node or None for the given path. + + Any new node needs to have the specified ``parent`` as a parent. :param path: a :py:class:`py.path.local` - the path to collect """ @@ -287,16 +289,16 @@ def pytest_collectstart(collector: "Collector") -> None: """ collector starts collecting. """ -def pytest_itemcollected(item): - """ we just collected a test item. """ +def pytest_itemcollected(item: "Item") -> None: + """We just collected a test item.""" def pytest_collectreport(report: "CollectReport") -> None: """ collector finished collecting. """ -def pytest_deselected(items): - """ called for test items deselected, e.g. by keyword. """ +def pytest_deselected(items: Sequence["Item"]) -> None: + """Called for deselected test items, e.g. by keyword.""" @hookspec(firstresult=True) @@ -312,13 +314,14 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor @hookspec(firstresult=True) -def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Optional[Module]": - """ return a Module collector or None for the given path. +def pytest_pycollect_makemodule(path: py.path.local, parent) -> Optional["Module"]: + """Return a Module collector or None for the given path. + This hook will be called for each matching test module path. The pytest_collect_file hook needs to be used if you want to create test modules for files that do not match as a test module. - Stops at first non-None result, see :ref:`firstresult` + Stops at first non-None result, see :ref:`firstresult`. :param path: a :py:class:`py.path.local` - the path of module to collect """ @@ -326,11 +329,12 @@ def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Optional[Module @hookspec(firstresult=True) def pytest_pycollect_makeitem( - collector: "PyCollector", name: str, obj -) -> "Union[None, Item, Collector, List[Union[Item, Collector]]]": - """ return custom item/collector for a python object in a module, or None. + collector: "PyCollector", name: str, obj: object +) -> Union[None, "Item", "Collector", List[Union["Item", "Collector"]]]: + """Return a custom item/collector for a Python object in a module, or None. - Stops at first non-None result, see :ref:`firstresult` """ + Stops at first non-None result, see :ref:`firstresult`. + """ @hookspec(firstresult=True) @@ -466,7 +470,7 @@ def pytest_runtest_call(item: "Item") -> None: """ -def pytest_runtest_teardown(item: "Item", nextitem: "Optional[Item]") -> None: +def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None: """Called to perform the teardown phase for a test item. The default implementation runs the finalizers and calls ``teardown()`` @@ -505,7 +509,9 @@ def pytest_runtest_logreport(report: "TestReport") -> None: @hookspec(firstresult=True) -def pytest_report_to_serializable(config: "Config", report: "BaseReport"): +def pytest_report_to_serializable( + config: "Config", report: Union["CollectReport", "TestReport"], +) -> Optional[Dict[str, Any]]: """ Serializes the given report object into a data structure suitable for sending over the wire, e.g. converted to JSON. @@ -513,7 +519,9 @@ def pytest_report_to_serializable(config: "Config", report: "BaseReport"): @hookspec(firstresult=True) -def pytest_report_from_serializable(config: "Config", data): +def pytest_report_from_serializable( + config: "Config", data: Dict[str, Any], +) -> Optional[Union["CollectReport", "TestReport"]]: """ Restores a report object previously serialized with pytest_report_to_serializable(). """ @@ -528,11 +536,11 @@ def pytest_report_from_serializable(config: "Config", data): def pytest_fixture_setup( fixturedef: "FixtureDef", request: "SubRequest" ) -> Optional[object]: - """ performs fixture setup execution. + """Performs fixture setup execution. - :return: The return value of the call to the fixture function + :return: The return value of the call to the fixture function. - Stops at first non-None result, see :ref:`firstresult` + Stops at first non-None result, see :ref:`firstresult`. .. note:: If the fixture function returns None, other implementations of @@ -555,7 +563,7 @@ def pytest_fixture_post_finalizer( def pytest_sessionstart(session: "Session") -> None: - """ called after the ``Session`` object has been created and before performing collection + """Called after the ``Session`` object has been created and before performing collection and entering the run test loop. :param _pytest.main.Session session: the pytest session object @@ -563,9 +571,9 @@ def pytest_sessionstart(session: "Session") -> None: def pytest_sessionfinish( - session: "Session", exitstatus: "Union[int, ExitCode]" + session: "Session", exitstatus: Union[int, "ExitCode"], ) -> None: - """ called after whole test run finished, right before returning the exit status to the system. + """Called after whole test run finished, right before returning the exit status to the system. :param _pytest.main.Session session: the pytest session object :param int exitstatus: the status which pytest will return to the system @@ -573,7 +581,7 @@ def pytest_sessionfinish( def pytest_unconfigure(config: "Config") -> None: - """ called before test process is exited. + """Called before test process is exited. :param _pytest.config.Config config: pytest config object """ @@ -587,7 +595,7 @@ def pytest_unconfigure(config: "Config") -> None: def pytest_assertrepr_compare( config: "Config", op: str, left: object, right: object ) -> Optional[List[str]]: - """return explanation for comparisons in failing assert expressions. + """Return explanation for comparisons in failing assert expressions. Return None for no custom explanation, otherwise return a list of strings. The strings will be joined by newlines but any newlines @@ -598,7 +606,7 @@ def pytest_assertrepr_compare( """ -def pytest_assertion_pass(item, lineno: int, orig: str, expl: str) -> None: +def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> None: """ **(Experimental)** @@ -665,12 +673,12 @@ def pytest_report_header( def pytest_report_collectionfinish( - config: "Config", startdir: py.path.local, items: "Sequence[Item]" + config: "Config", startdir: py.path.local, items: Sequence["Item"], ) -> Union[str, List[str]]: """ .. versionadded:: 3.2 - return a string or list of strings to be displayed after collection has finished successfully. + Return a string or list of strings to be displayed after collection has finished successfully. These strings will be displayed after the standard "collected X items" message. @@ -689,7 +697,7 @@ def pytest_report_collectionfinish( @hookspec(firstresult=True) def pytest_report_teststatus( - report: "BaseReport", config: "Config" + report: Union["CollectReport", "TestReport"], config: "Config" ) -> Tuple[ str, str, Union[str, Mapping[str, bool]], ]: @@ -734,7 +742,7 @@ def pytest_terminal_summary( def pytest_warning_captured( warning_message: "warnings.WarningMessage", when: "Literal['config', 'collect', 'runtest']", - item: "Optional[Item]", + item: Optional["Item"], location: Optional[Tuple[str, int, str]], ) -> None: """(**Deprecated**) Process a warning captured by the internal pytest warnings plugin. @@ -831,7 +839,9 @@ def pytest_keyboard_interrupt( def pytest_exception_interact( - node: "Node", call: "CallInfo[object]", report: "Union[CollectReport, TestReport]" + node: "Node", + call: "CallInfo[object]", + report: Union["CollectReport", "TestReport"], ) -> None: """Called when an exception was raised which can potentially be interactively handled. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index b7a3a958a31..98dabaf87d7 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -302,8 +302,8 @@ def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]: return None -def pytest_collection(session: "Session") -> Sequence[nodes.Item]: - return session.perform_collect() +def pytest_collection(session: "Session") -> None: + session.perform_collect() def pytest_runtestloop(session: "Session") -> bool: @@ -343,9 +343,7 @@ def _in_venv(path: py.path.local) -> bool: return any([fname.basename in activates for fname in bindir.listdir()]) -def pytest_ignore_collect( - path: py.path.local, config: Config -) -> "Optional[Literal[True]]": +def pytest_ignore_collect(path: py.path.local, config: Config) -> Optional[bool]: ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath()) ignore_paths = ignore_paths or [] excludeopt = config.getoption("ignore") diff --git a/src/_pytest/python.py b/src/_pytest/python.py index f3c42f42136..7fbd59addb1 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -422,7 +422,7 @@ def sort_key(item): return values def _makeitem( - self, name: str, obj + self, name: str, obj: object ) -> Union[ None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]] ]: diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 8b213ed1342..7aba0b0244f 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,6 +1,7 @@ from io import StringIO from pprint import pprint from typing import Any +from typing import Dict from typing import Iterable from typing import Iterator from typing import List @@ -69,7 +70,7 @@ def __init__(self, **kw: Any) -> None: def __getattr__(self, key: str) -> Any: raise NotImplementedError() - def toterminal(self, out) -> None: + def toterminal(self, out: TerminalWriter) -> None: if hasattr(self, "node"): out.line(getworkerinfoline(self.node)) @@ -187,7 +188,7 @@ def _get_verbose_word(self, config: Config): ) return verbose - def _to_json(self): + def _to_json(self) -> Dict[str, Any]: """ This was originally the serialize_report() function from xdist (ca03269). @@ -199,7 +200,7 @@ def _to_json(self): return _report_to_json(self) @classmethod - def _from_json(cls: "Type[_R]", reportdict) -> _R: + def _from_json(cls: "Type[_R]", reportdict: Dict[str, object]) -> _R: """ This was originally the serialize_report() function from xdist (ca03269). @@ -382,11 +383,13 @@ class CollectErrorRepr(TerminalRepr): def __init__(self, msg) -> None: self.longrepr = msg - def toterminal(self, out) -> None: + def toterminal(self, out: TerminalWriter) -> None: out.line(self.longrepr, red=True) -def pytest_report_to_serializable(report: BaseReport): +def pytest_report_to_serializable( + report: Union[CollectReport, TestReport] +) -> Optional[Dict[str, Any]]: if isinstance(report, (TestReport, CollectReport)): data = report._to_json() data["$report_type"] = report.__class__.__name__ @@ -394,7 +397,9 @@ def pytest_report_to_serializable(report: BaseReport): return None -def pytest_report_from_serializable(data) -> Optional[BaseReport]: +def pytest_report_from_serializable( + data: Dict[str, Any], +) -> Optional[Union[CollectReport, TestReport]]: if "$report_type" in data: if data["$report_type"] == "TestReport": return TestReport._from_json(data) @@ -406,7 +411,7 @@ def pytest_report_from_serializable(data) -> Optional[BaseReport]: return None -def _report_to_json(report: BaseReport): +def _report_to_json(report: BaseReport) -> Dict[str, Any]: """ This was originally the serialize_report() function from xdist (ca03269). @@ -414,7 +419,9 @@ def _report_to_json(report: BaseReport): serialization. """ - def serialize_repr_entry(entry: Union[ReprEntry, ReprEntryNative]): + def serialize_repr_entry( + entry: Union[ReprEntry, ReprEntryNative] + ) -> Dict[str, Any]: data = attr.asdict(entry) for key, value in data.items(): if hasattr(value, "__dict__"): @@ -422,25 +429,28 @@ def serialize_repr_entry(entry: Union[ReprEntry, ReprEntryNative]): entry_data = {"type": type(entry).__name__, "data": data} return entry_data - def serialize_repr_traceback(reprtraceback: ReprTraceback): + def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]: result = attr.asdict(reprtraceback) result["reprentries"] = [ serialize_repr_entry(x) for x in reprtraceback.reprentries ] return result - def serialize_repr_crash(reprcrash: Optional[ReprFileLocation]): + def serialize_repr_crash( + reprcrash: Optional[ReprFileLocation], + ) -> Optional[Dict[str, Any]]: if reprcrash is not None: return attr.asdict(reprcrash) else: return None - def serialize_longrepr(rep): + def serialize_longrepr(rep: BaseReport) -> Dict[str, Any]: + assert rep.longrepr is not None result = { "reprcrash": serialize_repr_crash(rep.longrepr.reprcrash), "reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback), "sections": rep.longrepr.sections, - } + } # type: Dict[str, Any] if isinstance(rep.longrepr, ExceptionChainRepr): result["chain"] = [] for repr_traceback, repr_crash, description in rep.longrepr.chain: @@ -473,7 +483,7 @@ def serialize_longrepr(rep): return d -def _report_kwargs_from_json(reportdict): +def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]: """ This was originally the serialize_report() function from xdist (ca03269). diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 6a58260e99f..9b10e5ffe28 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -467,9 +467,9 @@ def section(self, title: str, sep: str = "=", **kw: bool) -> None: def line(self, msg: str, **kw: bool) -> None: self._tw.line(msg, **kw) - def _add_stats(self, category: str, items: List) -> None: + def _add_stats(self, category: str, items: Sequence) -> None: set_main_color = category not in self.stats - self.stats.setdefault(category, []).extend(items[:]) + self.stats.setdefault(category, []).extend(items) if set_main_color: self._set_main_color() @@ -499,7 +499,7 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: # which garbles our output if we use self.write_line self.write_line(msg) - def pytest_deselected(self, items) -> None: + def pytest_deselected(self, items: Sequence[Item]) -> None: self._add_stats("deselected", items) def pytest_runtest_logstart( diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index a90b56c2962..0e4a3131167 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -44,7 +44,7 @@ def pytest_pycollect_makeitem( - collector: PyCollector, name: str, obj + collector: PyCollector, name: str, obj: object ) -> Optional["UnitTestCase"]: # has unittest been imported and is obj a subclass of its TestCase? try: From f382a6bb2084c8fb5a4e252ab7f3358752e27f67 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 25 Jun 2020 17:32:05 +0300 Subject: [PATCH 415/823] hookspec: remove unused hookspec pytest_doctest_prepare_content() It's been unused for 10 years at lest from bb50ec89a92f0623c9f8f5f29. --- changelog/7418.breaking.rst | 2 ++ src/_pytest/hookspec.py | 12 ------------ 2 files changed, 2 insertions(+), 12 deletions(-) create mode 100644 changelog/7418.breaking.rst diff --git a/changelog/7418.breaking.rst b/changelog/7418.breaking.rst new file mode 100644 index 00000000000..23f60da3765 --- /dev/null +++ b/changelog/7418.breaking.rst @@ -0,0 +1,2 @@ +Remove the `pytest_doctest_prepare_content` hook specification. This hook +hasn't been triggered by pytest for at least 10 years. diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 1b4b09c85d4..8c88b66cb17 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -805,18 +805,6 @@ def pytest_warning_recorded( """ -# ------------------------------------------------------------------------- -# doctest hooks -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) -def pytest_doctest_prepare_content(content): - """ return processed content for a given doctest - - Stops at first non-None result, see :ref:`firstresult` """ - - # ------------------------------------------------------------------------- # error handling and internal debugging hooks # ------------------------------------------------------------------------- From ba50ef33d323fce32f5a357c2449dcf9c3119d2d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 25 Jun 2020 17:32:34 +0200 Subject: [PATCH 416/823] Add open training at Workshoptage 2020 --- doc/en/talks.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/en/talks.rst b/doc/en/talks.rst index 26df77c295c..50af51e71ff 100644 --- a/doc/en/talks.rst +++ b/doc/en/talks.rst @@ -2,6 +2,10 @@ Talks and Tutorials ========================== +.. sidebar:: Next Open Trainings + + - `"pytest: Test Driven Development (nicht nur) für Python" `_ (German) at the `CH Open Workshoptage `_, September 8 2020, HSLU Campus Rotkreuz (ZG), Switzerland. + .. _`funcargs`: funcargs.html Books From 97d2c711e60b6c782be9422143f04ef38dfe2b29 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Fri, 26 Jun 2020 14:46:37 +0200 Subject: [PATCH 417/823] Reduce calls of Node.ihook --- src/_pytest/assertion/__init__.py | 10 +++++----- src/_pytest/runner.py | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index f404607c1d0..64d2267e70a 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -124,6 +124,8 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: comparison for the test. """ + ihook = item.ihook + def callbinrepr(op, left: object, right: object) -> Optional[str]: """Call the pytest_assertrepr_compare hook and prepare the result @@ -139,7 +141,7 @@ def callbinrepr(op, left: object, right: object) -> Optional[str]: The result can be formatted by util.format_explanation() for pretty printing. """ - hook_result = item.ihook.pytest_assertrepr_compare( + hook_result = ihook.pytest_assertrepr_compare( config=item.config, op=op, left=left, right=right ) for new_expl in hook_result: @@ -155,12 +157,10 @@ def callbinrepr(op, left: object, right: object) -> Optional[str]: saved_assert_hooks = util._reprcompare, util._assertion_pass util._reprcompare = callbinrepr - if item.ihook.pytest_assertion_pass.get_hookimpls(): + if ihook.pytest_assertion_pass.get_hookimpls(): def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None: - item.ihook.pytest_assertion_pass( - item=item, lineno=lineno, orig=orig, expl=expl - ) + ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl) util._assertion_pass = call_assertion_pass_hook diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index b6c89dce5f5..8b23cb49e55 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -95,9 +95,10 @@ def pytest_sessionfinish(session: "Session") -> None: def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool: - item.ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) + ihook = item.ihook + ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) runtestprotocol(item, nextitem=nextitem) - item.ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) + ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) return True From 1ae4182e1836000eb35a40ec4c3dbb2689e0c5ae Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 26 Jun 2020 15:50:19 +0300 Subject: [PATCH 418/823] testing: fix flaky tests on pypy3 due to resource warnings in stderr (#7405) --- testing/test_stepwise.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 3bc77857d97..df66d798bbe 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -75,6 +75,16 @@ def broken_testdir(testdir): return testdir +def _strip_resource_warnings(lines): + # Strip unreliable ResourceWarnings, so no-output assertions on stderr can work. + # (https://github.com/pytest-dev/pytest/issues/5088) + return [ + x + for x in lines + if not x.startswith(("Exception ignored in:", "ResourceWarning")) + ] + + def test_run_without_stepwise(stepwise_testdir): result = stepwise_testdir.runpytest("-v", "--strict-markers", "--fail") @@ -88,7 +98,7 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): result = stepwise_testdir.runpytest( "-v", "--strict-markers", "--stepwise", "--fail" ) - assert not result.stderr.str() + assert _strip_resource_warnings(result.stderr.lines) == [] stdout = result.stdout.str() # Make sure we stop after first failing test. @@ -98,7 +108,7 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): # "Fix" the test that failed in the last run and run it again. result = stepwise_testdir.runpytest("-v", "--strict-markers", "--stepwise") - assert not result.stderr.str() + assert _strip_resource_warnings(result.stderr.lines) == [] stdout = result.stdout.str() # Make sure the latest failing test runs and then continues. @@ -116,7 +126,7 @@ def test_run_with_skip_option(stepwise_testdir): "--fail", "--fail-last", ) - assert not result.stderr.str() + assert _strip_resource_warnings(result.stderr.lines) == [] stdout = result.stdout.str() # Make sure first fail is ignore and second fail stops the test run. @@ -129,7 +139,7 @@ def test_run_with_skip_option(stepwise_testdir): def test_fail_on_errors(error_testdir): result = error_testdir.runpytest("-v", "--strict-markers", "--stepwise") - assert not result.stderr.str() + assert _strip_resource_warnings(result.stderr.lines) == [] stdout = result.stdout.str() assert "test_error ERROR" in stdout @@ -140,7 +150,7 @@ def test_change_testfile(stepwise_testdir): result = stepwise_testdir.runpytest( "-v", "--strict-markers", "--stepwise", "--fail", "test_a.py" ) - assert not result.stderr.str() + assert _strip_resource_warnings(result.stderr.lines) == [] stdout = result.stdout.str() assert "test_fail_on_flag FAILED" in stdout @@ -150,7 +160,7 @@ def test_change_testfile(stepwise_testdir): result = stepwise_testdir.runpytest( "-v", "--strict-markers", "--stepwise", "test_b.py" ) - assert not result.stderr.str() + assert _strip_resource_warnings(result.stderr.lines) == [] stdout = result.stdout.str() assert "test_success PASSED" in stdout From 103bfd20d49089f042fa556c299800a17115d30c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2020 17:08:14 +0200 Subject: [PATCH 419/823] Add webinar --- doc/en/talks.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/en/talks.rst b/doc/en/talks.rst index 50af51e71ff..253dfe78ed8 100644 --- a/doc/en/talks.rst +++ b/doc/en/talks.rst @@ -4,6 +4,7 @@ Talks and Tutorials .. sidebar:: Next Open Trainings + - `Free 1h webinar: "pytest: Test Driven Development für Python" `_ (German), online, August 18 2020. - `"pytest: Test Driven Development (nicht nur) für Python" `_ (German) at the `CH Open Workshoptage `_, September 8 2020, HSLU Campus Rotkreuz (ZG), Switzerland. .. _`funcargs`: funcargs.html From 03230b4002b0cf88a00b4a1fc6f15f77d99cda7e Mon Sep 17 00:00:00 2001 From: gdhameeja Date: Thu, 4 Jun 2020 01:38:11 +0530 Subject: [PATCH 420/823] Fix-6906: Added code-highlight option to disable highlighting optionally Co-authored-by: Ran Benita --- changelog/6906.feature.rst | 1 + src/_pytest/_io/terminalwriter.py | 3 ++- src/_pytest/config/__init__.py | 7 ++++++- src/_pytest/terminal.py | 6 ++++++ testing/io/test_terminalwriter.py | 21 +++++++++++++++++---- 5 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 changelog/6906.feature.rst diff --git a/changelog/6906.feature.rst b/changelog/6906.feature.rst new file mode 100644 index 00000000000..3e1fe3ef175 --- /dev/null +++ b/changelog/6906.feature.rst @@ -0,0 +1 @@ +Added `--code-highlight` command line option to enable/disable code highlighting in terminal output. diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index a285cf4fc36..70bb2e2dcd6 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -74,6 +74,7 @@ def __init__(self, file: Optional[TextIO] = None) -> None: self.hasmarkup = should_do_markup(file) self._current_line = "" self._terminal_width = None # type: Optional[int] + self.code_highlight = True @property def fullwidth(self) -> int: @@ -180,7 +181,7 @@ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> No def _highlight(self, source: str) -> str: """Highlight the given source code if we have markup support.""" - if not self.hasmarkup: + if not self.hasmarkup or not self.code_highlight: return source try: from pygments.formatters.terminal import TerminalFormatter diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 717743e7946..bf5b780823b 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1389,8 +1389,13 @@ def create_terminal_writer( tw = TerminalWriter(file=file) if config.option.color == "yes": tw.hasmarkup = True - if config.option.color == "no": + elif config.option.color == "no": tw.hasmarkup = False + + if config.option.code_highlight == "yes": + tw.code_highlight = True + elif config.option.code_highlight == "no": + tw.code_highlight = False return tw diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 6a58260e99f..9dbd477eafe 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -208,6 +208,12 @@ def pytest_addoption(parser: Parser) -> None: choices=["yes", "no", "auto"], help="color terminal output (yes/no/auto).", ) + group._addoption( + "--code-highlight", + default="yes", + choices=["yes", "no"], + help="Whether code should be highlighted (only if --color is also enabled)", + ) parser.addini( "console_output_style", diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py index 0e9cdb64d06..94cff307fcd 100644 --- a/testing/io/test_terminalwriter.py +++ b/testing/io/test_terminalwriter.py @@ -213,19 +213,32 @@ def test_combining(self) -> None: @pytest.mark.parametrize( - "has_markup, expected", + ("has_markup", "code_highlight", "expected"), [ pytest.param( - True, "{kw}assert{hl-reset} {number}0{hl-reset}\n", id="with markup" + True, + True, + "{kw}assert{hl-reset} {number}0{hl-reset}\n", + id="with markup and code_highlight", + ), + pytest.param( + True, False, "assert 0\n", id="with markup but no code_highlight", + ), + pytest.param( + False, True, "assert 0\n", id="without markup but with code_highlight", + ), + pytest.param( + False, False, "assert 0\n", id="neither markup nor code_highlight", ), - pytest.param(False, "assert 0\n", id="no markup"), ], ) -def test_code_highlight(has_markup, expected, color_mapping): +def test_code_highlight(has_markup, code_highlight, expected, color_mapping): f = io.StringIO() tw = terminalwriter.TerminalWriter(f) tw.hasmarkup = has_markup + tw.code_highlight = code_highlight tw._write_source(["assert 0"]) + assert f.getvalue().splitlines(keepends=True) == color_mapping.format([expected]) with pytest.raises( From 289197ff228af8ddd51e51fff6bee6bdd93cc780 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 27 Jun 2020 11:16:50 -0300 Subject: [PATCH 421/823] Remove package scope experimental status Close #7389 --- changelog/7389.trivial.rst | 1 + doc/en/fixture.rst | 34 ++++++++++++++-------------------- src/_pytest/fixtures.py | 3 +-- 3 files changed, 16 insertions(+), 22 deletions(-) create mode 100644 changelog/7389.trivial.rst diff --git a/changelog/7389.trivial.rst b/changelog/7389.trivial.rst new file mode 100644 index 00000000000..00cfe92bcee --- /dev/null +++ b/changelog/7389.trivial.rst @@ -0,0 +1 @@ +Fixture scope ``package`` is no longer considered experimental. diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index a4e262c2fa3..883cdcd4390 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -244,8 +244,8 @@ and `pytest-datafiles `__. .. _smtpshared: -Scope: sharing a fixture instance across tests in a class, module or session ----------------------------------------------------------------------------- +Scope: sharing fixtures across classes, modules, packages or session +-------------------------------------------------------------------- .. regendoc:wipe @@ -356,29 +356,23 @@ instance, you can simply declare it: # all tests needing it ... -Finally, the ``class`` scope will invoke the fixture once per test *class*. -.. note:: - - Pytest will only cache one instance of a fixture at a time. - This means that when using a parametrized fixture, pytest may invoke a fixture more than once in the given scope. - - -``package`` scope (experimental) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Fixture scopes +^^^^^^^^^^^^^^ +Fixtures are created when first requested by a test, and are destroyed based on their ``scope``: +* ``function``: the default scope, the fixture is destroyed at the end of the test. +* ``class``: the fixture is destroyed during teardown of the last test in the class. +* ``module``: the fixture is destroyed during teardown of the last test in the module. +* ``package``: the fixture is destroyed during teardown of the last test in the package. +* ``session``: the fixture is destroyed at the end of the test session. -In pytest 3.7 the ``package`` scope has been introduced. Package-scoped fixtures -are finalized when the last test of a *package* finishes. - -.. warning:: - This functionality is considered **experimental** and may be removed in future - versions if hidden corner-cases or serious problems with this functionality - are discovered after it gets more usage in the wild. - - Use this new feature sparingly and please make sure to report any issues you find. +.. note:: + Pytest only caches one instance of a fixture at a time, which + means that when using a parametrized fixture, pytest may invoke a fixture more than once in + the given scope. .. _dynamic scope: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 9423df7e4c3..4aebdb95155 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1274,8 +1274,7 @@ def fixture( # noqa: F811 :arg scope: the scope for which this fixture is shared, one of ``"function"`` (default), ``"class"``, ``"module"``, - ``"package"`` or ``"session"`` (``"package"`` is considered **experimental** - at this time). + ``"package"`` or ``"session"``. This parameter may also be a callable which receives ``(fixture_name, config)`` as parameters, and must return a ``str`` with one of the values mentioned above. From c39655725a086b07432995149e43f1884ab1d754 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 27 Jun 2020 19:49:19 -0400 Subject: [PATCH 422/823] change if else structure of _warn_bout_missing_assertion --- src/_pytest/config/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 98c5fd0b4e5..9ed37295911 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1360,19 +1360,20 @@ def _warn_about_missing_assertion(self, mode: str) -> None: if not _assertion_supported(): from _pytest.warnings import _issue_warning_captured - warning_text = ( - "assertions not in test modules or" - " plugins will be ignored" - " because assert statements are not executed " - "by the underlying Python interpreter " - "(are you using python -O?)\n" - ) if mode == "plain": warning_text = ( "ASSERTIONS ARE NOT EXECUTED" " and FAILING TESTS WILL PASS. Are you" " using python -O?" ) + else: + warning_text = ( + "assertions not in test modules or" + " plugins will be ignored" + " because assert statements are not executed " + "by the underlying Python interpreter " + "(are you using python -O?)\n" + ) _issue_warning_captured( PytestConfigWarning(warning_text), self.hook, stacklevel=2, ) From 49ec2aed0f326fc5fa25dbdd7c47bf0a888dbd97 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sun, 28 Jun 2020 10:48:33 -0400 Subject: [PATCH 423/823] change stacklevel in warnings from 2 to 3 --- src/_pytest/config/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 9ed37295911..b5cff73016a 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1186,7 +1186,7 @@ def _warn_or_fail_if_strict(self, message: str) -> None: from _pytest.warnings import _issue_warning_captured _issue_warning_captured( - PytestConfigWarning(message), self.hook, stacklevel=2, + PytestConfigWarning(message), self.hook, stacklevel=3, ) def _get_unknown_ini_keys(self) -> List[str]: @@ -1375,7 +1375,7 @@ def _warn_about_missing_assertion(self, mode: str) -> None: "(are you using python -O?)\n" ) _issue_warning_captured( - PytestConfigWarning(warning_text), self.hook, stacklevel=2, + PytestConfigWarning(warning_text), self.hook, stacklevel=3, ) From e492b1d567f74588e712265dd8016b2156cf6afa Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 30 Jun 2020 00:05:12 +0300 Subject: [PATCH 424/823] python: don't pass entire Item for generating ID Just the nodeid is enough for the error messages. This removes an import cycle. --- src/_pytest/mark/structures.py | 7 ++----- src/_pytest/python.py | 29 +++++++++++++++------------ testing/python/metafunc.py | 36 ++++++++-------------------------- 3 files changed, 27 insertions(+), 45 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 2d756bb420a..ca5c03e901e 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -27,9 +27,6 @@ from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning -if TYPE_CHECKING: - from _pytest.python import FunctionDefinition - EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" @@ -159,7 +156,7 @@ def _for_parametrize( argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], func, config: Config, - function_definition: "FunctionDefinition", + nodeid: str, ) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]: argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) @@ -177,7 +174,7 @@ def _for_parametrize( ) fail( msg.format( - nodeid=function_definition.nodeid, + nodeid=nodeid, values=param.values, names=argnames, names_len=len(argnames), diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 7fbd59addb1..65855ca2371 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -980,7 +980,7 @@ def parametrize( argvalues, self.function, self.config, - function_definition=self.definition, + nodeid=self.definition.nodeid, ) del argvalues @@ -1003,7 +1003,9 @@ def parametrize( if generated_ids is not None: ids = generated_ids - ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition) + ids = self._resolve_arg_ids( + argnames, ids, parameters, nodeid=self.definition.nodeid + ) # Store used (possibly generated) ids with parametrize Marks. if _param_mark and _param_mark._param_ids_from and generated_ids is None: @@ -1042,7 +1044,7 @@ def _resolve_arg_ids( ] ], parameters: typing.Sequence[ParameterSet], - item, + nodeid: str, ) -> List[str]: """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given to ``parametrize``. @@ -1050,7 +1052,7 @@ def _resolve_arg_ids( :param List[str] argnames: list of argument names passed to ``parametrize()``. :param ids: the ids parameter of the parametrized call (see docs). :param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``. - :param Item item: the item that generated this parametrized call. + :param str str: the nodeid of the item that generated this parametrized call. :rtype: List[str] :return: the list of ids for each argname given """ @@ -1063,7 +1065,7 @@ def _resolve_arg_ids( else: idfn = None ids_ = self._validate_ids(ids, parameters, self.function.__name__) - return idmaker(argnames, parameters, idfn, ids_, self.config, item=item) + return idmaker(argnames, parameters, idfn, ids_, self.config, nodeid=nodeid) def _validate_ids( self, @@ -1223,7 +1225,7 @@ def _idval( argname: str, idx: int, idfn: Optional[Callable[[object], Optional[object]]], - item, + nodeid: Optional[str], config: Optional[Config], ) -> str: if idfn: @@ -1232,8 +1234,9 @@ def _idval( if generated_id is not None: val = generated_id except Exception as e: - msg = "{}: error raised while trying to determine id of parameter '{}' at position {}" - msg = msg.format(item.nodeid, argname, idx) + prefix = "{}: ".format(nodeid) if nodeid is not None else "" + msg = "error raised while trying to determine id of parameter '{}' at position {}" + msg = prefix + msg.format(argname, idx) raise ValueError(msg) from e elif config: hook_id = config.hook.pytest_make_parametrize_id( @@ -1263,7 +1266,7 @@ def _idvalset( argnames: Iterable[str], idfn: Optional[Callable[[object], Optional[object]]], ids: Optional[List[Union[None, str]]], - item, + nodeid: Optional[str], config: Optional[Config], ): if parameterset.id is not None: @@ -1271,7 +1274,7 @@ def _idvalset( id = None if ids is None or idx >= len(ids) else ids[idx] if id is None: this_id = [ - _idval(val, argname, idx, idfn, item=item, config=config) + _idval(val, argname, idx, idfn, nodeid=nodeid, config=config) for val, argname in zip(parameterset.values, argnames) ] return "-".join(this_id) @@ -1285,10 +1288,12 @@ def idmaker( idfn: Optional[Callable[[object], Optional[object]]] = None, ids: Optional[List[Union[None, str]]] = None, config: Optional[Config] = None, - item=None, + nodeid: Optional[str] = None, ) -> List[str]: resolved_ids = [ - _idvalset(valindex, parameterset, argnames, idfn, ids, config=config, item=item) + _idvalset( + valindex, parameterset, argnames, idfn, ids, config=config, nodeid=nodeid + ) for valindex, parameterset in enumerate(parametersets) ] diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index c4b5bd22295..1f110d41954 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -19,6 +19,7 @@ from _pytest.outcomes import fail from _pytest.pytester import Testdir from _pytest.python import _idval +from _pytest.python import idmaker class TestMetafunc: @@ -35,10 +36,11 @@ def __init__(self, names): @attr.s class DefinitionMock(python.FunctionDefinition): obj = attr.ib() + _nodeid = attr.ib() names = fixtures.getfuncargnames(func) fixtureinfo = FuncFixtureInfoMock(names) # type: Any - definition = DefinitionMock._create(func) # type: Any + definition = DefinitionMock._create(func, "mock::nodeid") # type: Any return python.Metafunc(definition, fixtureinfo, config) def test_no_funcargs(self) -> None: @@ -270,7 +272,7 @@ class A: deadline=400.0 ) # very close to std deadline and CI boxes are not reliable in CPU power def test_idval_hypothesis(self, value) -> None: - escaped = _idval(value, "a", 6, None, item=None, config=None) + escaped = _idval(value, "a", 6, None, nodeid=None, config=None) assert isinstance(escaped, str) escaped.encode("ascii") @@ -292,7 +294,7 @@ def test_unicode_idval(self) -> None: ), ] for val, expected in values: - assert _idval(val, "a", 6, None, item=None, config=None) == expected + assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected def test_unicode_idval_with_config(self) -> None: """unittest for expected behavior to obtain ids with @@ -321,7 +323,7 @@ def getini(self, name): ("ação", MockConfig({option: False}), "a\\xe7\\xe3o"), ] # type: List[Tuple[str, Any, str]] for val, config, expected in values: - actual = _idval(val, "a", 6, None, item=None, config=config) + actual = _idval(val, "a", 6, None, nodeid=None, config=config) assert actual == expected def test_bytes_idval(self) -> None: @@ -338,7 +340,7 @@ def test_bytes_idval(self) -> None: ("αρά".encode(), "\\xce\\xb1\\xcf\\x81\\xce\\xac"), ] for val, expected in values: - assert _idval(val, "a", 6, idfn=None, item=None, config=None) == expected + assert _idval(val, "a", 6, idfn=None, nodeid=None, config=None) == expected def test_class_or_function_idval(self) -> None: """unittest for the expected behavior to obtain ids for parametrized @@ -353,12 +355,10 @@ def test_function(): values = [(TestClass, "TestClass"), (test_function, "test_function")] for val, expected in values: - assert _idval(val, "a", 6, None, item=None, config=None) == expected + assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected def test_idmaker_autoname(self) -> None: """#250""" - from _pytest.python import idmaker - result = idmaker( ("a", "b"), [pytest.param("string", 1.0), pytest.param("st-ring", 2.0)] ) @@ -373,14 +373,10 @@ def test_idmaker_autoname(self) -> None: assert result == ["a0-\\xc3\\xb4"] def test_idmaker_with_bytes_regex(self) -> None: - from _pytest.python import idmaker - result = idmaker(("a"), [pytest.param(re.compile(b"foo"), 1.0)]) assert result == ["foo"] def test_idmaker_native_strings(self) -> None: - from _pytest.python import idmaker - result = idmaker( ("a", "b"), [ @@ -414,8 +410,6 @@ def test_idmaker_native_strings(self) -> None: ] def test_idmaker_non_printable_characters(self) -> None: - from _pytest.python import idmaker - result = idmaker( ("s", "n"), [ @@ -430,8 +424,6 @@ def test_idmaker_non_printable_characters(self) -> None: assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4", "\\t-5", "\\t-6"] def test_idmaker_manual_ids_must_be_printable(self) -> None: - from _pytest.python import idmaker - result = idmaker( ("s",), [ @@ -442,8 +434,6 @@ def test_idmaker_manual_ids_must_be_printable(self) -> None: assert result == ["hello \\x00", "hello \\x05"] def test_idmaker_enum(self) -> None: - from _pytest.python import idmaker - enum = pytest.importorskip("enum") e = enum.Enum("Foo", "one, two") result = idmaker(("a", "b"), [pytest.param(e.one, e.two)]) @@ -451,7 +441,6 @@ def test_idmaker_enum(self) -> None: def test_idmaker_idfn(self) -> None: """#351""" - from _pytest.python import idmaker def ids(val: object) -> Optional[str]: if isinstance(val, Exception): @@ -471,7 +460,6 @@ def ids(val: object) -> Optional[str]: def test_idmaker_idfn_unique_names(self) -> None: """#351""" - from _pytest.python import idmaker def ids(val: object) -> str: return "a" @@ -492,7 +480,6 @@ def test_idmaker_with_idfn_and_config(self) -> None: disable_test_id_escaping_and_forfeit_all_rights_to_community_support option. (#5294) """ - from _pytest.python import idmaker class MockConfig: def __init__(self, config): @@ -525,7 +512,6 @@ def test_idmaker_with_ids_and_config(self) -> None: disable_test_id_escaping_and_forfeit_all_rights_to_community_support option. (#5294) """ - from _pytest.python import idmaker class MockConfig: def __init__(self, config): @@ -607,16 +593,12 @@ def test_int(arg): ) def test_idmaker_with_ids(self) -> None: - from _pytest.python import idmaker - result = idmaker( ("a", "b"), [pytest.param(1, 2), pytest.param(3, 4)], ids=["a", None] ) assert result == ["a", "3-4"] def test_idmaker_with_paramset_id(self) -> None: - from _pytest.python import idmaker - result = idmaker( ("a", "b"), [pytest.param(1, 2, id="me"), pytest.param(3, 4, id="you")], @@ -625,8 +607,6 @@ def test_idmaker_with_paramset_id(self) -> None: assert result == ["me", "you"] def test_idmaker_with_ids_unique_names(self) -> None: - from _pytest.python import idmaker - result = idmaker( ("a"), map(pytest.param, [1, 2, 3, 4, 5]), ids=["a", "a", "b", "c", "b"] ) From 40c355f8c3554a2002976d9f5426ca53293ccf59 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 29 Jun 2020 23:31:01 +0300 Subject: [PATCH 425/823] python: pytest_pycollect_makeitem doesn't need to be a hookwrapper A trylast is more appropriate for this usecase. hookwrappers are more complicated and more expensive than regular hookimpls, so better avoided. --- src/_pytest/python.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 65855ca2371..c5cd14bdc05 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -209,16 +209,12 @@ def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module": return mod -@hookimpl(hookwrapper=True) -def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj): - outcome = yield - res = outcome.get_result() - if res is not None: - return +@hookimpl(trylast=True) +def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object): # nothing was collected elsewhere, let's do it here if safe_isclass(obj): if collector.istestclass(obj, name): - outcome.force_result(Class.from_parent(collector, name=name, obj=obj)) + return Class.from_parent(collector, name=name, obj=obj) elif collector.istestfunction(obj, name): # mock seems to store unbound methods (issue473), normalize it obj = getattr(obj, "__func__", obj) @@ -245,7 +241,7 @@ def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj): res.warn(PytestCollectionWarning(reason)) else: res = list(collector._genfunctions(name, obj)) - outcome.force_result(res) + return res class PyobjMixin: From ae83dbd4cfd08c027d9d9771db8aaa0474836ac6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 16 Jun 2020 12:50:09 +0300 Subject: [PATCH 426/823] python: remove ancient Function.repr_failure(outerr) parameter This has been asserted like this since 04e9197fd6138adaf953ba8fef370 (i.e. 11 years, pytest 1.0), seems safe to simply remove at this point. --- src/_pytest/python.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c5cd14bdc05..751e174071a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1590,9 +1590,8 @@ def _prunetraceback(self, excinfo: ExceptionInfo) -> None: # TODO: Type ignored -- breaks Liskov Substitution. def repr_failure( # type: ignore[override] # noqa: F821 - self, excinfo: ExceptionInfo[BaseException], outerr: None = None + self, excinfo: ExceptionInfo[BaseException], ) -> Union[str, TerminalRepr]: - assert outerr is None, "XXX outerr usage is deprecated" style = self.config.getoption("tbstyle", "auto") if style == "auto": style = "long" From 304f2916fcdce20ad9f353210b9c2ada6051ee79 Mon Sep 17 00:00:00 2001 From: Ruaridh Williamson Date: Thu, 30 Apr 2020 09:30:43 +0100 Subject: [PATCH 427/823] logging: use unique handlers for caplog and reports Setting log_level via the CLI or .ini will control the log level of the report that is dumped upon failure of a test. If caplog modified the log level during the execution of that test, it should not impact the level that is displayed upon failure in the "captured log report" section. [ ran: - rebased - reused handler - changed store keys also to "caplog_handler_*" - added changelog all bugs are mine :) ] --- changelog/7159.improvement.rst | 3 ++ src/_pytest/logging.py | 35 +++++++++++++--------- testing/logging/test_fixture.py | 53 ++++++++++++++++++++++++++++----- 3 files changed, 70 insertions(+), 21 deletions(-) create mode 100644 changelog/7159.improvement.rst diff --git a/changelog/7159.improvement.rst b/changelog/7159.improvement.rst new file mode 100644 index 00000000000..c5f51a7b721 --- /dev/null +++ b/changelog/7159.improvement.rst @@ -0,0 +1,3 @@ +When the ``caplog`` fixture is used to change the log level for capturing, +using ``caplog.set_level()`` or ``caplog.at_level()``, it no longer affects +the level of logs that are shown in the "Captured log report" report section. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 57aa14f2774..52d75e66d9f 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -34,8 +34,8 @@ DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" _ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m") -catch_log_handler_key = StoreKey["LogCaptureHandler"]() -catch_log_records_key = StoreKey[Dict[str, List[logging.LogRecord]]]() +caplog_handler_key = StoreKey["LogCaptureHandler"]() +caplog_records_key = StoreKey[Dict[str, List[logging.LogRecord]]]() def _remove_ansi_escape_sequences(text: str) -> str: @@ -362,7 +362,7 @@ def handler(self) -> LogCaptureHandler: """ :rtype: LogCaptureHandler """ - return self._item._store[catch_log_handler_key] + return self._item._store[caplog_handler_key] def get_records(self, when: str) -> List[logging.LogRecord]: """ @@ -376,7 +376,7 @@ def get_records(self, when: str) -> List[logging.LogRecord]: .. versionadded:: 3.4 """ - return self._item._store[catch_log_records_key].get(when, []) + return self._item._store[caplog_records_key].get(when, []) @property def text(self) -> str: @@ -523,8 +523,10 @@ def __init__(self, config: Config) -> None: get_option_ini(config, "log_auto_indent"), ) self.log_level = get_log_level_for_setting(config, "log_level") - self.log_handler = LogCaptureHandler() - self.log_handler.setFormatter(self.formatter) + self.caplog_handler = LogCaptureHandler() + self.caplog_handler.setFormatter(self.formatter) + self.report_handler = LogCaptureHandler() + self.report_handler.setFormatter(self.formatter) # File logging. self.log_file_level = get_log_level_for_setting(config, "log_file_level") @@ -665,14 +667,19 @@ def pytest_runtest_logreport(self) -> None: def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]: """Implements the internals of pytest_runtest_xxx() hook.""" - with catching_logs(self.log_handler, level=self.log_level) as log_handler: - log_handler.reset() - item._store[catch_log_records_key][when] = log_handler.records - item._store[catch_log_handler_key] = log_handler + with catching_logs( + self.caplog_handler, level=self.log_level, + ) as caplog_handler, catching_logs( + self.report_handler, level=self.log_level, + ) as report_handler: + caplog_handler.reset() + report_handler.reset() + item._store[caplog_records_key][when] = caplog_handler.records + item._store[caplog_handler_key] = caplog_handler yield - log = log_handler.stream.getvalue().strip() + log = report_handler.stream.getvalue().strip() item.add_report_section(when, "log", log) @pytest.hookimpl(hookwrapper=True) @@ -680,7 +687,7 @@ def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("setup") empty = {} # type: Dict[str, List[logging.LogRecord]] - item._store[catch_log_records_key] = empty + item._store[caplog_records_key] = empty yield from self._runtest_for(item, "setup") @pytest.hookimpl(hookwrapper=True) @@ -694,8 +701,8 @@ def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, Non self.log_cli_handler.set_when("teardown") yield from self._runtest_for(item, "teardown") - del item._store[catch_log_records_key] - del item._store[catch_log_handler_key] + del item._store[caplog_records_key] + del item._store[caplog_handler_key] @pytest.hookimpl def pytest_runtest_logfinish(self) -> None: diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index 3a3663464c2..da5303302b6 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -1,7 +1,7 @@ import logging import pytest -from _pytest.logging import catch_log_records_key +from _pytest.logging import caplog_records_key logger = logging.getLogger(__name__) sublogger = logging.getLogger(__name__ + ".baz") @@ -137,7 +137,7 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"] # This reaches into private API, don't use this type of thing in real tests! - assert set(caplog._item._store[catch_log_records_key]) == {"setup", "call"} + assert set(caplog._item._store[caplog_records_key]) == {"setup", "call"} def test_ini_controls_global_log_level(testdir): @@ -216,12 +216,10 @@ def test_log_level_override(request, caplog): plugin = request.config.pluginmanager.getplugin('logging-plugin') assert plugin.log_level == logging.WARNING - logger.info("INFO message won't be shown") - - caplog.set_level(logging.INFO, logger.name) + logger.error("ERROR message " + "will be shown") with caplog.at_level(logging.DEBUG, logger.name): - logger.debug("DEBUG message will be shown") + logger.debug("DEBUG message " + "won't be shown") raise Exception() """ ) @@ -233,5 +231,46 @@ def test_log_level_override(request, caplog): ) result = testdir.runpytest() - result.stdout.fnmatch_lines(["*DEBUG message will be shown*"]) + result.stdout.fnmatch_lines(["*ERROR message will be shown*"]) + result.stdout.no_fnmatch_line("*DEBUG message won't be shown*") + assert result.ret == 1 + + +def test_log_report_captures_according_to_config_option_upon_failure(testdir): + """ Test that upon failure: + (1) `caplog` succeeded to capture the DEBUG message and assert on it => No `Exception` is raised + (2) The `DEBUG` message does NOT appear in the `Captured log call` report + (3) The stdout, `INFO`, and `WARNING` messages DO appear in the test reports due to `--log-level=INFO` + """ + testdir.makepyfile( + """ + import pytest + import logging + + def function_that_logs(): + logging.debug('DEBUG log ' + 'message') + logging.info('INFO log ' + 'message') + logging.warning('WARNING log ' + 'message') + print('Print ' + 'message') + + def test_that_fails(request, caplog): + plugin = request.config.pluginmanager.getplugin('logging-plugin') + assert plugin.log_level == logging.INFO + + with caplog.at_level(logging.DEBUG): + function_that_logs() + + if 'DEBUG log ' + 'message' not in caplog.text: + raise Exception('caplog failed to ' + 'capture DEBUG') + + assert False + """ + ) + + result = testdir.runpytest("--log-level=INFO") + result.stdout.no_fnmatch_line("*Exception: caplog failed to capture DEBUG*") + result.stdout.no_fnmatch_line("*DEBUG log message*") + result.stdout.fnmatch_lines( + ["*Print message*", "*INFO log message*", "*WARNING log message*"] + ) assert result.ret == 1 From 7b1ba7c0db5ad655342b1c4dc5b45005912b06dc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Apr 2020 23:42:31 +0300 Subject: [PATCH 428/823] pytester: slightly clean up LsofFdLeakChecker --- src/_pytest/pytester.py | 50 +++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index fd4c10577a8..45f6f008ab1 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -94,21 +94,16 @@ def pytest_configure(config: Config) -> None: class LsofFdLeakChecker: - def get_open_files(self): - out = self._exec_lsof() - open_files = self._parse_lsof_output(out) - return open_files - - def _exec_lsof(self): - pid = os.getpid() - # py3: use subprocess.DEVNULL directly. - with open(os.devnull, "wb") as devnull: - return subprocess.check_output( - ("lsof", "-Ffn0", "-p", str(pid)), stderr=devnull - ).decode() - - def _parse_lsof_output(self, out): - def isopen(line): + def get_open_files(self) -> List[Tuple[str, str]]: + out = subprocess.run( + ("lsof", "-Ffn0", "-p", str(os.getpid())), + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=True, + universal_newlines=True, + ).stdout + + def isopen(line: str) -> bool: return line.startswith("f") and ( "deleted" not in line and "mem" not in line @@ -130,9 +125,9 @@ def isopen(line): return open_files - def matching_platform(self): + def matching_platform(self) -> bool: try: - subprocess.check_output(("lsof", "-v")) + subprocess.run(("lsof", "-v"), check=True) except (OSError, subprocess.CalledProcessError): return False else: @@ -149,16 +144,17 @@ def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: 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 = [] - error.append("***** %s FD leakage detected" % len(leaked_files)) - error.extend([str(f) for f in leaked_files]) - error.append("*** Before:") - error.extend([str(f) for f in lines1]) - error.append("*** After:") - error.extend([str(f) for f in lines2]) - error.append(error[0]) - error.append("*** function %s:%s: %s " % item.location) - error.append("See issue #2366") + 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(pytest.PytestWarning("\n".join(error))) From 2fe178488acbfe8eed32727680c884bacbcec7c2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:20:06 +0300 Subject: [PATCH 429/823] code/source: expose deindent kwarg in signature Probably was done to avoid the shadowing issue, but work around it instead. --- src/_pytest/_code/source.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 2ccbaf657c2..1c69498ea70 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -31,9 +31,8 @@ class Source: _compilecounter = 0 - def __init__(self, *parts, **kwargs) -> None: + def __init__(self, *parts, deindent: bool = True) -> None: self.lines = lines = [] # type: List[str] - de = kwargs.get("deindent", True) for part in parts: if not part: partlines = [] # type: List[str] @@ -44,9 +43,9 @@ def __init__(self, *parts, **kwargs) -> None: elif isinstance(part, str): partlines = part.split("\n") else: - partlines = getsource(part, deindent=de).lines - if de: - partlines = deindent(partlines) + partlines = getsource(part, deindent=deindent).lines + if deindent: + partlines = _deindent_function(partlines) lines.extend(partlines) def __eq__(self, other): @@ -307,20 +306,24 @@ def getrawcode(obj, trycall: bool = True): return obj -def getsource(obj, **kwargs) -> Source: +def getsource(obj, *, deindent: bool = True) -> Source: obj = getrawcode(obj) try: strsrc = inspect.getsource(obj) except IndentationError: strsrc = '"Buggy python version consider upgrading, cannot get source"' assert isinstance(strsrc, str) - return Source(strsrc, **kwargs) + return Source(strsrc, deindent=deindent) def deindent(lines: Sequence[str]) -> List[str]: return textwrap.dedent("\n".join(lines)).splitlines() +# Internal alias to avoid shadowing with `deindent` parameter. +_deindent_function = deindent + + def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]: # flatten all statements and except handlers into one lineno-list # AST's line numbers start indexing at 1 From 410817477763bd27c3fc72f1d3227e84df1e2a35 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:20:07 +0300 Subject: [PATCH 430/823] code/source: remove Source(deindent: bool) parameter Not used, except in tests. --- src/_pytest/_code/source.py | 20 ++++++++------------ testing/code/test_source.py | 11 ++++++----- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 1c69498ea70..0bc2e243e2d 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -25,13 +25,14 @@ class Source: - """ an immutable object holding a source code fragment, - possibly deindenting it. + """An immutable object holding a source code fragment. + + When using Source(...), the source lines are deindented. """ _compilecounter = 0 - def __init__(self, *parts, deindent: bool = True) -> None: + def __init__(self, *parts) -> None: self.lines = lines = [] # type: List[str] for part in parts: if not part: @@ -43,9 +44,8 @@ def __init__(self, *parts, deindent: bool = True) -> None: elif isinstance(part, str): partlines = part.split("\n") else: - partlines = getsource(part, deindent=deindent).lines - if deindent: - partlines = _deindent_function(partlines) + partlines = getsource(part).lines + partlines = deindent(partlines) lines.extend(partlines) def __eq__(self, other): @@ -306,24 +306,20 @@ def getrawcode(obj, trycall: bool = True): return obj -def getsource(obj, *, deindent: bool = True) -> Source: +def getsource(obj) -> Source: obj = getrawcode(obj) try: strsrc = inspect.getsource(obj) except IndentationError: strsrc = '"Buggy python version consider upgrading, cannot get source"' assert isinstance(strsrc, str) - return Source(strsrc, deindent=deindent) + return Source(strsrc) def deindent(lines: Sequence[str]) -> List[str]: return textwrap.dedent("\n".join(lines)).splitlines() -# Internal alias to avoid shadowing with `deindent` parameter. -_deindent_function = deindent - - def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]: # flatten all statements and except handlers into one lineno-list # AST's line numbers start indexing at 1 diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 35728c33443..014034dec90 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -4,6 +4,7 @@ import ast import inspect import sys +import textwrap from types import CodeType from typing import Any from typing import Dict @@ -64,8 +65,6 @@ def test_source_from_inner_function() -> None: def f(): pass - source = _pytest._code.Source(f, deindent=False) - assert str(source).startswith(" def f():") source = _pytest._code.Source(f) assert str(source).startswith("def f():") @@ -557,7 +556,7 @@ def __call__(self) -> None: def getstatement(lineno: int, source) -> Source: from _pytest._code.source import getstatementrange_ast - src = _pytest._code.Source(source, deindent=False) + src = _pytest._code.Source(source) ast, start, end = getstatementrange_ast(lineno, src) return src[start:end] @@ -633,7 +632,7 @@ def deco_mark(): assert False src = inspect.getsource(deco_mark) - assert str(Source(deco_mark, deindent=False)) == src + assert textwrap.indent(str(Source(deco_mark)), " ") + "\n" == src assert src.startswith(" @pytest.mark.foo") @pytest.fixture @@ -646,7 +645,9 @@ def deco_fixture(): # existing behavior here for explicitness, but perhaps we should revisit/change this # in the future assert str(Source(deco_fixture)).startswith("@functools.wraps(function)") - assert str(Source(get_real_func(deco_fixture), deindent=False)) == src + assert ( + textwrap.indent(str(Source(get_real_func(deco_fixture))), " ") + "\n" == src + ) def test_single_line_else() -> None: From c6083ab970e436b14987cbc7074fc3a894943fcc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:20:08 +0300 Subject: [PATCH 431/823] code/source: remove old IndentationError workaround in getsource() This has been there since as far as the git history goes (2007), is not covered by any test, and says "Buggy python version consider upgrading". Hopefully everyone have upgraded... --- src/_pytest/_code/source.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 0bc2e243e2d..eb4c4df78e6 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -308,10 +308,7 @@ def getrawcode(obj, trycall: bool = True): def getsource(obj) -> Source: obj = getrawcode(obj) - try: - strsrc = inspect.getsource(obj) - except IndentationError: - strsrc = '"Buggy python version consider upgrading, cannot get source"' + strsrc = inspect.getsource(obj) assert isinstance(strsrc, str) return Source(strsrc) From c83e16ab2e3218c8bf12293fd6d8d7e68d959ecf Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:20:09 +0300 Subject: [PATCH 432/823] code/source: remove unneeded assert inspect.getsource() definitely returns str. --- src/_pytest/_code/source.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index eb4c4df78e6..1089b6ef0f0 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -309,7 +309,6 @@ def getrawcode(obj, trycall: bool = True): def getsource(obj) -> Source: obj = getrawcode(obj) strsrc = inspect.getsource(obj) - assert isinstance(strsrc, str) return Source(strsrc) From 2b99bfbc60a0f344e6ef2c73e2dc5a8b0d9d764f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:20:09 +0300 Subject: [PATCH 433/823] code/source: remove support for passing multiple parts to Source It isn't used, so keep it simple. --- src/_pytest/_code/source.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 1089b6ef0f0..6cc12320251 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -8,10 +8,10 @@ from bisect import bisect_right from types import CodeType from types import FrameType +from typing import Iterable from typing import Iterator from typing import List from typing import Optional -from typing import Sequence from typing import Tuple from typing import Union @@ -32,21 +32,17 @@ class Source: _compilecounter = 0 - def __init__(self, *parts) -> None: - self.lines = lines = [] # type: List[str] - for part in parts: - if not part: - partlines = [] # type: List[str] - elif isinstance(part, Source): - partlines = part.lines - elif isinstance(part, (tuple, list)): - partlines = [x.rstrip("\n") for x in part] - elif isinstance(part, str): - partlines = part.split("\n") - else: - partlines = getsource(part).lines - partlines = deindent(partlines) - lines.extend(partlines) + def __init__(self, obj: object = None) -> None: + if not obj: + self.lines = [] # type: List[str] + elif isinstance(obj, Source): + self.lines = obj.lines + elif isinstance(obj, (tuple, list)): + self.lines = deindent(x.rstrip("\n") for x in obj) + elif isinstance(obj, str): + self.lines = deindent(obj.split("\n")) + else: + self.lines = deindent(getsource(obj).lines) def __eq__(self, other): try: @@ -312,7 +308,7 @@ def getsource(obj) -> Source: return Source(strsrc) -def deindent(lines: Sequence[str]) -> List[str]: +def deindent(lines: Iterable[str]) -> List[str]: return textwrap.dedent("\n".join(lines)).splitlines() From a127a22d13ce637b10244f2bf60e0df0b0313f57 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:20:10 +0300 Subject: [PATCH 434/823] code/source: remove support for comparing Source with str Cross-type comparisons like this are a bad idea. This isn't used. --- src/_pytest/_code/source.py | 11 ++++------- testing/code/test_code.py | 3 ++- testing/code/test_source.py | 8 ++++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 6cc12320251..f7dcdeff956 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -44,13 +44,10 @@ def __init__(self, obj: object = None) -> None: else: self.lines = deindent(getsource(obj).lines) - def __eq__(self, other): - try: - return self.lines == other.lines - except AttributeError: - if isinstance(other, str): - return str(self) == other - return False + def __eq__(self, other: object) -> bool: + if not isinstance(other, Source): + return NotImplemented + return self.lines == other.lines # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 5cbd899905b..25a3e9aeb59 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -6,6 +6,7 @@ from _pytest._code import Code from _pytest._code import ExceptionInfo from _pytest._code import Frame +from _pytest._code import Source from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ReprFuncArgs @@ -67,7 +68,7 @@ def func() -> FrameType: f = Frame(func()) with mock.patch.object(f.code.__class__, "fullsource", None): - assert f.statement == "" + assert f.statement == Source("") def test_code_from_func() -> None: diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 014034dec90..8616b2f25b5 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -227,9 +227,9 @@ def test_getstatementrange_triple_quoted(self) -> None: ''')""" ) s = source.getstatement(0) - assert s == str(source) + assert s == source s = source.getstatement(1) - assert s == str(source) + assert s == source def test_getstatementrange_within_constructs(self) -> None: source = Source( @@ -445,7 +445,7 @@ def test_getsource_fallback() -> None: expected = """def x(): pass""" src = getsource(x) - assert src == expected + assert str(src) == expected def test_idem_compile_and_getsource() -> None: @@ -454,7 +454,7 @@ def test_idem_compile_and_getsource() -> None: expected = "def x(): pass" co = _pytest._code.compile(expected) src = getsource(co) - assert src == expected + assert str(src) == expected def test_compile_ast() -> None: From a7303b52db2ae319324ec4f09c470ff1f932cf7b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:20:11 +0300 Subject: [PATCH 435/823] code/source: remove unused method Source.isparseable() --- src/_pytest/_code/source.py | 15 --------------- testing/code/test_source.py | 10 ---------- 2 files changed, 25 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index f7dcdeff956..6c9aaa7e647 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -132,21 +132,6 @@ def deindent(self) -> "Source": newsource.lines[:] = deindent(self.lines) return newsource - def isparseable(self, deindent: bool = True) -> bool: - """ return True if source is parseable, heuristically - deindenting it by default. - """ - if deindent: - source = str(self.deindent()) - else: - source = str(self) - try: - ast.parse(source) - except (SyntaxError, ValueError, TypeError): - return False - else: - return True - def __str__(self) -> str: return "\n".join(self.lines) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 8616b2f25b5..0bf8c0b17b0 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -125,15 +125,6 @@ def test_syntaxerror_rerepresentation() -> None: assert ex.value.text.rstrip("\n") == "xyz xyz" -def test_isparseable() -> None: - assert Source("hello").isparseable() - assert Source("if 1:\n pass").isparseable() - assert Source(" \nif 1:\n pass").isparseable() - assert not Source("if 1:\n").isparseable() - assert not Source(" \nif 1:\npass").isparseable() - assert not Source(chr(0)).isparseable() - - class TestAccesses: def setup_class(self) -> None: self.source = Source( @@ -147,7 +138,6 @@ def g(x): def test_getrange(self) -> None: x = self.source[0:2] - assert x.isparseable() assert len(x.lines) == 2 assert str(x) == "def f(x):\n pass" From 4a27d7d9738e4281a28c8449363a9b89705267fb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:20:11 +0300 Subject: [PATCH 436/823] code/source: remove unused method Source.putaround() --- src/_pytest/_code/source.py | 13 ------------- testing/code/test_source.py | 33 --------------------------------- 2 files changed, 46 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 6c9aaa7e647..019da5765ff 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -89,19 +89,6 @@ def strip(self) -> "Source": source.lines[:] = self.lines[start:end] return source - def putaround( - self, before: str = "", after: str = "", indent: str = " " * 4 - ) -> "Source": - """ return a copy of the source object with - 'before' and 'after' wrapped around it. - """ - beforesource = Source(before) - aftersource = Source(after) - newsource = Source() - lines = [(indent + line) for line in self.lines] - newsource.lines = beforesource.lines + lines + aftersource.lines - return newsource - def indent(self, indent: str = " " * 4) -> "Source": """ return a copy of the source object with all lines indented by the given indent-string. diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 0bf8c0b17b0..97a00964b09 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -69,39 +69,6 @@ def f(): assert str(source).startswith("def f():") -def test_source_putaround_simple() -> None: - source = Source("raise ValueError") - source = source.putaround( - "try:", - """\ - except ValueError: - x = 42 - else: - x = 23""", - ) - assert ( - str(source) - == """\ -try: - raise ValueError -except ValueError: - x = 42 -else: - x = 23""" - ) - - -def test_source_putaround() -> None: - source = Source() - source = source.putaround( - """ - if 1: - x=1 - """ - ) - assert str(source).strip() == "if 1:\n x=1" - - def test_source_strips() -> None: source = Source("") assert source == Source() From 9640c9c9eb2f1ccdfea67dbfe541bdf3b0b8a128 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:20:12 +0300 Subject: [PATCH 437/823] skipping: use plain compile() instead of _pytest._code.compile() eval() is used for evaluating string conditions in skipif/xfail e.g. @pytest.mark.skipif("1 == 0") This is the only code that uses `_pytest._code.compile()`, so removing its last use enables us to remove it entirely. In this case it doesn't add much. Plain compile() gives a good enough error message. For regular exceptions, the message is the same. For SyntaxError exceptions, e.g. "1 ==", the previous code adds a little bit of useful context: ``` invalid syntax (skipping.py:108>, line 1) The above exception was the direct cause of the following exception: 1 == ^ (code was compiled probably from here: <0-codegen /pytest/src/_pytest/skipping.py:108>) (line 1) During handling of the above exception, another exception occurred: Error evaluating 'skipif' condition 1 == ^ SyntaxError: invalid syntax ``` The new code loses it: ``` unexpected EOF while parsing (, line 1) During handling of the above exception, another exception occurred: Error evaluating 'skipif' condition 1 == ^ SyntaxError: invalid syntax ``` Since the old message is a minor improvement to an unlikely error condition in a deprecated feature, I think it is not worth all the code that it requires. --- src/_pytest/skipping.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 7bd975e5a09..a72bdaabf27 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -9,7 +9,6 @@ import attr -import _pytest._code from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import hookimpl @@ -105,7 +104,8 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, if hasattr(item, "obj"): globals_.update(item.obj.__globals__) # type: ignore[attr-defined] try: - condition_code = _pytest._code.compile(condition, mode="eval") + filename = "<{} condition>".format(mark.name) + condition_code = compile(condition, filename, "eval") result = eval(condition_code, globals_) except SyntaxError as exc: msglines = [ From ef39115001b70990ab7dee6fda61d9f1fec6a293 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:20:12 +0300 Subject: [PATCH 438/823] code/source: remove compiling functions A lot of complex code that isn't used anymore outside of tests after the previous commit. --- src/_pytest/_code/__init__.py | 2 - src/_pytest/_code/source.py | 130 ------------------------------- testing/code/test_excinfo.py | 74 +++++++++--------- testing/code/test_source.py | 140 +++++----------------------------- 4 files changed, 56 insertions(+), 290 deletions(-) diff --git a/src/_pytest/_code/__init__.py b/src/_pytest/_code/__init__.py index 76963c0eb59..136da31959e 100644 --- a/src/_pytest/_code/__init__.py +++ b/src/_pytest/_code/__init__.py @@ -7,7 +7,6 @@ from .code import getrawcode from .code import Traceback from .code import TracebackEntry -from .source import compile_ as compile from .source import Source __all__ = [ @@ -19,6 +18,5 @@ "getrawcode", "Traceback", "TracebackEntry", - "compile", "Source", ] diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 019da5765ff..6cb602a9328 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -1,13 +1,9 @@ import ast import inspect -import linecache -import sys import textwrap import tokenize import warnings from bisect import bisect_right -from types import CodeType -from types import FrameType from typing import Iterable from typing import Iterator from typing import List @@ -15,13 +11,7 @@ from typing import Tuple from typing import Union -import py - from _pytest.compat import overload -from _pytest.compat import TYPE_CHECKING - -if TYPE_CHECKING: - from typing_extensions import Literal class Source: @@ -30,8 +20,6 @@ class Source: When using Source(...), the source lines are deindented. """ - _compilecounter = 0 - def __init__(self, obj: object = None) -> None: if not obj: self.lines = [] # type: List[str] @@ -122,124 +110,6 @@ def deindent(self) -> "Source": def __str__(self) -> str: return "\n".join(self.lines) - @overload - def compile( - self, - filename: Optional[str] = ..., - mode: str = ..., - flag: "Literal[0]" = ..., - dont_inherit: int = ..., - _genframe: Optional[FrameType] = ..., - ) -> CodeType: - raise NotImplementedError() - - @overload # noqa: F811 - def compile( # noqa: F811 - self, - filename: Optional[str] = ..., - mode: str = ..., - flag: int = ..., - dont_inherit: int = ..., - _genframe: Optional[FrameType] = ..., - ) -> Union[CodeType, ast.AST]: - raise NotImplementedError() - - def compile( # noqa: F811 - self, - filename: Optional[str] = None, - mode: str = "exec", - flag: int = 0, - dont_inherit: int = 0, - _genframe: Optional[FrameType] = None, - ) -> Union[CodeType, ast.AST]: - """ return compiled code object. if filename is None - invent an artificial filename which displays - the source/line position of the caller frame. - """ - if not filename or py.path.local(filename).check(file=0): - if _genframe is None: - _genframe = sys._getframe(1) # the caller - fn, lineno = _genframe.f_code.co_filename, _genframe.f_lineno - base = "<%d-codegen " % self._compilecounter - self.__class__._compilecounter += 1 - if not filename: - filename = base + "%s:%d>" % (fn, lineno) - else: - filename = base + "%r %s:%d>" % (filename, fn, lineno) - source = "\n".join(self.lines) + "\n" - try: - co = compile(source, filename, mode, flag) - except SyntaxError as ex: - # re-represent syntax errors from parsing python strings - msglines = self.lines[: ex.lineno] - if ex.offset: - msglines.append(" " * ex.offset + "^") - msglines.append("(code was compiled probably from here: %s)" % filename) - newex = SyntaxError("\n".join(msglines)) - newex.offset = ex.offset - newex.lineno = ex.lineno - newex.text = ex.text - raise newex from ex - else: - if flag & ast.PyCF_ONLY_AST: - assert isinstance(co, ast.AST) - return co - assert isinstance(co, CodeType) - lines = [(x + "\n") for x in self.lines] - # Type ignored because linecache.cache is private. - linecache.cache[filename] = (1, None, lines, filename) # type: ignore - return co - - -# -# public API shortcut functions -# - - -@overload -def compile_( - source: Union[str, bytes, ast.mod, ast.AST], - filename: Optional[str] = ..., - mode: str = ..., - flags: "Literal[0]" = ..., - dont_inherit: int = ..., -) -> CodeType: - raise NotImplementedError() - - -@overload # noqa: F811 -def compile_( # noqa: F811 - source: Union[str, bytes, ast.mod, ast.AST], - filename: Optional[str] = ..., - mode: str = ..., - flags: int = ..., - dont_inherit: int = ..., -) -> Union[CodeType, ast.AST]: - raise NotImplementedError() - - -def compile_( # noqa: F811 - source: Union[str, bytes, ast.mod, ast.AST], - filename: Optional[str] = None, - mode: str = "exec", - flags: int = 0, - dont_inherit: int = 0, -) -> Union[CodeType, ast.AST]: - """ compile the given source to a raw code object, - and maintain an internal cache which allows later - retrieval of the source code for the code object - and any recursively created code objects. - """ - if isinstance(source, ast.AST): - # XXX should Source support having AST? - assert filename is not None - co = compile(source, filename, mode, flags, dont_inherit) - assert isinstance(co, (CodeType, ast.AST)) - return co - _genframe = sys._getframe(1) # the caller - s = Source(source) - return s.compile(filename, mode, flags, _genframe=_genframe) - # # helper functions diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 75c937612e9..52d5286b8ad 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -127,24 +127,28 @@ def test_traceback_entry_getsource(self): assert s.endswith("raise ValueError") def test_traceback_entry_getsource_in_construct(self): - source = _pytest._code.Source( - """\ - def xyz(): - try: - raise ValueError - except somenoname: - pass - xyz() - """ - ) + def xyz(): + try: + raise ValueError + except somenoname: # type: ignore[name-defined] # noqa: F821 + pass + try: - exec(source.compile()) + xyz() except NameError: - tb = _pytest._code.ExceptionInfo.from_current().traceback - print(tb[-1].getsource()) - s = str(tb[-1].getsource()) - assert s.startswith("def xyz():\n try:") - assert s.strip().endswith("except somenoname:") + excinfo = _pytest._code.ExceptionInfo.from_current() + else: + assert False, "did not raise NameError" + + tb = excinfo.traceback + source = tb[-1].getsource() + assert source is not None + assert source.deindent().lines == [ + "def xyz():", + " try:", + " raise ValueError", + " except somenoname: # type: ignore[name-defined] # noqa: F821", + ] def test_traceback_cut(self): co = _pytest._code.Code(f) @@ -445,16 +449,6 @@ def importasmod(source): return importasmod - def excinfo_from_exec(self, source): - source = _pytest._code.Source(source).strip() - try: - exec(source.compile()) - except KeyboardInterrupt: - raise - except BaseException: - return _pytest._code.ExceptionInfo.from_current() - assert 0, "did not raise" - def test_repr_source(self): pr = FormattedExcinfo() source = _pytest._code.Source( @@ -471,19 +465,29 @@ def f(x): def test_repr_source_excinfo(self) -> None: """ check if indentation is right """ - pr = FormattedExcinfo() - excinfo = self.excinfo_from_exec( - """ - def f(): - assert 0 - f() - """ - ) + try: + + def f(): + 1 / 0 + + f() + + except BaseException: + excinfo = _pytest._code.ExceptionInfo.from_current() + else: + assert 0, "did not raise" + pr = FormattedExcinfo() source = pr._getentrysource(excinfo.traceback[-1]) assert source is not None lines = pr.get_source(source, 1, excinfo) - assert lines == [" def f():", "> assert 0", "E AssertionError"] + for line in lines: + print(line) + assert lines == [ + " def f():", + "> 1 / 0", + "E ZeroDivisionError: division by zero", + ] def test_repr_source_not_existing(self): pr = FormattedExcinfo() diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 97a00964b09..11f2f53cfd6 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -3,6 +3,7 @@ # or redundant on purpose and can't be disable on a line-by-line basis import ast import inspect +import linecache import sys import textwrap from types import CodeType @@ -33,14 +34,6 @@ def test_source_str_function() -> None: assert str(x) == "\n3" -def test_unicode() -> None: - x = Source("4") - assert str(x) == "4" - co = _pytest._code.compile('"å"', mode="eval") - val = eval(co) - assert isinstance(val, str) - - def test_source_from_function() -> None: source = _pytest._code.Source(test_source_str_function) assert str(source).startswith("def test_source_str_function() -> None:") @@ -83,15 +76,6 @@ def test_source_strip_multiline() -> None: assert source2.lines == [" hello"] -def test_syntaxerror_rerepresentation() -> None: - ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz") - assert ex is not None - assert ex.value.lineno == 1 - assert ex.value.offset in {5, 7} # cpython: 7, pypy3.6 7.1.1: 5 - assert ex.value.text - assert ex.value.text.rstrip("\n") == "xyz xyz" - - class TestAccesses: def setup_class(self) -> None: self.source = Source( @@ -124,7 +108,7 @@ def test_iter(self) -> None: assert len(values) == 4 -class TestSourceParsingAndCompiling: +class TestSourceParsing: def setup_class(self) -> None: self.source = Source( """\ @@ -135,39 +119,6 @@ def f(x): """ ).strip() - def test_compile(self) -> None: - co = _pytest._code.compile("x=3") - d = {} # type: Dict[str, Any] - exec(co, d) - assert d["x"] == 3 - - def test_compile_and_getsource_simple(self) -> None: - co = _pytest._code.compile("x=3") - exec(co) - source = _pytest._code.Source(co) - assert str(source) == "x=3" - - def test_compile_and_getsource_through_same_function(self) -> None: - def gensource(source): - return _pytest._code.compile(source) - - co1 = gensource( - """ - def f(): - raise KeyError() - """ - ) - co2 = gensource( - """ - def f(): - raise ValueError() - """ - ) - source1 = inspect.getsource(co1) - assert "KeyError" in source1 - source2 = inspect.getsource(co2) - assert "ValueError" in source2 - def test_getstatement(self) -> None: # print str(self.source) ass = str(self.source[1:]) @@ -264,44 +215,6 @@ def test_getstatementrange_with_syntaxerror_issue7(self) -> None: source = Source(":") pytest.raises(SyntaxError, lambda: source.getstatementrange(0)) - def test_compile_to_ast(self) -> None: - source = Source("x = 4") - mod = source.compile(flag=ast.PyCF_ONLY_AST) - assert isinstance(mod, ast.Module) - compile(mod, "", "exec") - - def test_compile_and_getsource(self) -> None: - co = self.source.compile() - exec(co, globals()) - f(7) # type: ignore - excinfo = pytest.raises(AssertionError, f, 6) # type: ignore - assert excinfo is not None - frame = excinfo.traceback[-1].frame - assert isinstance(frame.code.fullsource, Source) - stmt = frame.code.fullsource.getstatement(frame.lineno) - assert str(stmt).strip().startswith("assert") - - @pytest.mark.parametrize("name", ["", None, "my"]) - def test_compilefuncs_and_path_sanity(self, name: Optional[str]) -> None: - def check(comp, name) -> None: - co = comp(self.source, name) - if not name: - expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2) # type: ignore - else: - expected = "codegen %r %s:%d>" % (name, mypath, mylineno + 2 + 2) # type: ignore - fn = co.co_filename - assert fn.endswith(expected) - - mycode = _pytest._code.Code(self.test_compilefuncs_and_path_sanity) - mylineno = mycode.firstlineno - mypath = mycode.path - - for comp in _pytest._code.compile, _pytest._code.Source.compile: - check(comp, name) - - def test_offsetless_synerr(self): - pytest.raises(SyntaxError, _pytest._code.compile, "lambda a,a: 0", mode="eval") - def test_getstartingblock_singleline() -> None: class A: @@ -331,18 +244,16 @@ def c() -> None: def test_getfuncsource_dynamic() -> None: - source = """ - def f(): - raise ValueError + def f(): + raise ValueError - def g(): pass - """ - co = _pytest._code.compile(source) - exec(co, globals()) - f_source = _pytest._code.Source(f) # type: ignore - g_source = _pytest._code.Source(g) # type: ignore + def g(): + pass + + f_source = _pytest._code.Source(f) + g_source = _pytest._code.Source(g) assert str(f_source).strip() == "def f():\n raise ValueError" - assert str(g_source).strip() == "def g(): pass" + assert str(g_source).strip() == "def g():\n pass" def test_getfuncsource_with_multine_string() -> None: @@ -405,23 +316,6 @@ def test_getsource_fallback() -> None: assert str(src) == expected -def test_idem_compile_and_getsource() -> None: - from _pytest._code.source import getsource - - expected = "def x(): pass" - co = _pytest._code.compile(expected) - src = getsource(co) - assert str(src) == expected - - -def test_compile_ast() -> None: - # We don't necessarily want to support this. - # This test was added just for coverage. - stmt = ast.parse("def x(): pass") - co = _pytest._code.compile(stmt, filename="foo.py") - assert isinstance(co, CodeType) - - def test_findsource_fallback() -> None: from _pytest._code.source import findsource @@ -431,15 +325,15 @@ def test_findsource_fallback() -> None: assert src[lineno] == " def x():" -def test_findsource() -> None: +def test_findsource(monkeypatch) -> None: from _pytest._code.source import findsource - co = _pytest._code.compile( - """if 1: - def x(): - pass -""" - ) + filename = "" + lines = ["if 1:\n", " def x():\n", " pass\n"] + co = compile("".join(lines), filename, "exec") + + # Type ignored because linecache.cache is private. + monkeypatch.setitem(linecache.cache, filename, (1, None, lines, filename)) # type: ignore[attr-defined] src, lineno = findsource(co) assert src is not None From f5c69f3eb2ea43d363e30c73b83209e13e953139 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:20:13 +0300 Subject: [PATCH 439/823] code/source: inline getsource() The recursive way in which Source and getsource interact is a bit confusing, just inline it. --- src/_pytest/_code/source.py | 10 +++------- testing/code/test_source.py | 6 ++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 6cb602a9328..65560be2a5e 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -30,7 +30,9 @@ def __init__(self, obj: object = None) -> None: elif isinstance(obj, str): self.lines = deindent(obj.split("\n")) else: - self.lines = deindent(getsource(obj).lines) + rawcode = getrawcode(obj) + src = inspect.getsource(rawcode) + self.lines = deindent(src.split("\n")) def __eq__(self, other: object) -> bool: if not isinstance(other, Source): @@ -141,12 +143,6 @@ def getrawcode(obj, trycall: bool = True): return obj -def getsource(obj) -> Source: - obj = getrawcode(obj) - strsrc = inspect.getsource(obj) - return Source(strsrc) - - def deindent(lines: Iterable[str]) -> List[str]: return textwrap.dedent("\n".join(lines)).splitlines() diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 11f2f53cfd6..ea5b7a4a577 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -307,12 +307,10 @@ def x(): pass -def test_getsource_fallback() -> None: - from _pytest._code.source import getsource - +def test_source_fallback() -> None: + src = Source(x) expected = """def x(): pass""" - src = getsource(x) assert str(src) == expected From c8cfff6de5d57c59d7a394f45678386f4d0b5015 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Jul 2020 20:08:56 +0300 Subject: [PATCH 440/823] testing: fix flaky tests due to "coroutine never awaited" warnings They sometime leak into other test's warnings and cause them to fail. --- testing/acceptance_test.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 66c2bf0bfd4..e558c7f6781 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1160,6 +1160,9 @@ def test_usage_error_code(testdir): @pytest.mark.filterwarnings("default") def test_warn_on_async_function(testdir): + # In the below we .close() the coroutine only to avoid + # "RuntimeWarning: coroutine 'test_2' was never awaited" + # which messes with other tests. testdir.makepyfile( test_async=""" async def test_1(): @@ -1167,7 +1170,9 @@ async def test_1(): async def test_2(): pass def test_3(): - return test_2() + coro = test_2() + coro.close() + return coro """ ) result = testdir.runpytest() From 04d052e30612b45330ff2fe83595e372fa21f9bd Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Thu, 2 Jul 2020 15:31:03 -0400 Subject: [PATCH 441/823] fix mypy issue by using typing Match instead of re.Match --- src/_pytest/junitxml.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 4df7535de8e..ee44d95567e 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -16,6 +16,7 @@ from datetime import datetime from typing import Dict from typing import List +from typing import Match from typing import Optional from typing import Tuple from typing import Union @@ -70,7 +71,7 @@ class Junit(py.xml.Namespace): def bin_xml_escape(arg: str) -> py.xml.raw: - def repl(matchobj: "re.Match[str]") -> str: + def repl(matchobj: "Match[str]") -> str: i = ord(matchobj.group()) if i <= 0xFF: return "#x%02X" % i From e596b26f1ae2901b72410ab3ed3984d59376dbeb Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Thu, 2 Jul 2020 18:22:54 -0400 Subject: [PATCH 442/823] Don't quote the Match type since it's imported from Typing --- src/_pytest/junitxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index ee44d95567e..71a31b85b76 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -71,7 +71,7 @@ class Junit(py.xml.Namespace): def bin_xml_escape(arg: str) -> py.xml.raw: - def repl(matchobj: "Match[str]") -> str: + def repl(matchobj: Match[str]) -> str: i = ord(matchobj.group()) if i <= 0xFF: return "#x%02X" % i From 40301effb8ffa3859efe9fca7f460e1af522d275 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 4 Jul 2020 11:45:28 +0300 Subject: [PATCH 443/823] Add changelog entry for code/source changes --- changelog/7438.breaking.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 changelog/7438.breaking.rst diff --git a/changelog/7438.breaking.rst b/changelog/7438.breaking.rst new file mode 100644 index 00000000000..5d5d239fc99 --- /dev/null +++ b/changelog/7438.breaking.rst @@ -0,0 +1,11 @@ +Some changes were made to the internal ``_pytest._code.source``, listed here +for the benefit of plugin authors who may be using it: + +- The ``deindent`` argument to ``Source()`` has been removed, now it is always true. +- Support for zero or multiple arguments to ``Source()`` has been removed. +- Support for comparing ``Source`` with an ``str`` has been removed. +- The methods ``Source.isparseable()`` and ``Source.putaround()`` have been removed. +- The method ``Source.compile()`` and function ``_pytest._code.compile()`` have + been removed; use plain ``compile()`` instead. +- The function ``_pytest._code.source.getsource()`` has been removed; use + ``Source()`` directly instead. From 11efe057ea0a46341c14ab230145a7c8accbc30c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 4 Jul 2020 12:12:52 +0300 Subject: [PATCH 444/823] testing: skip some unreachable code in coverage --- testing/code/test_excinfo.py | 4 ++-- testing/code/test_source.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 52d5286b8ad..6ee848e54ae 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -131,7 +131,7 @@ def xyz(): try: raise ValueError except somenoname: # type: ignore[name-defined] # noqa: F821 - pass + pass # pragma: no cover try: xyz() @@ -475,7 +475,7 @@ def f(): except BaseException: excinfo = _pytest._code.ExceptionInfo.from_current() else: - assert 0, "did not raise" + assert False, "did not raise" pr = FormattedExcinfo() source = pr._getentrysource(excinfo.traceback[-1]) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index ea5b7a4a577..4222eb172f2 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -56,7 +56,7 @@ def test_source_from_lines() -> None: def test_source_from_inner_function() -> None: def f(): - pass + raise NotImplementedError() source = _pytest._code.Source(f) assert str(source).startswith("def f():") @@ -245,15 +245,15 @@ def c() -> None: def test_getfuncsource_dynamic() -> None: def f(): - raise ValueError + raise NotImplementedError() def g(): - pass + pass # pragma: no cover f_source = _pytest._code.Source(f) g_source = _pytest._code.Source(g) - assert str(f_source).strip() == "def f():\n raise ValueError" - assert str(g_source).strip() == "def g():\n pass" + assert str(f_source).strip() == "def f():\n raise NotImplementedError()" + assert str(g_source).strip() == "def g():\n pass # pragma: no cover" def test_getfuncsource_with_multine_string() -> None: From 2bcad38fbd1fb4022e2dc261817b9918cfbda43e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 4 Jul 2020 12:30:40 +0300 Subject: [PATCH 445/823] Publish our types --- changelog/3342.feature.rst | 10 ++++++++++ setup.cfg | 4 ++++ src/_pytest/py.typed | 0 src/pytest/py.typed | 0 4 files changed, 14 insertions(+) create mode 100644 changelog/3342.feature.rst create mode 100644 src/_pytest/py.typed create mode 100644 src/pytest/py.typed diff --git a/changelog/3342.feature.rst b/changelog/3342.feature.rst new file mode 100644 index 00000000000..aef7e2b0466 --- /dev/null +++ b/changelog/3342.feature.rst @@ -0,0 +1,10 @@ +pytest now includes inline type annotations and exposes them to user programs. +Most of the user-facing API is covered, as well as internal code. + +If you are running a type checker such as mypy on your tests, you may start +noticing type errors indicating incorrect usage. If you run into an error that +you believe to be incorrect, please let us know in an issue. + +The types were developed against mypy version 0.780. Older versions may work, +but we recommend using at least this version. Other type checkers may work as +well, but they are not officially verified to work by pytest yet. diff --git a/setup.cfg b/setup.cfg index 3e5cfa1f869..31123f28e2e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -75,6 +75,10 @@ testing = requests xmlschema +[options.package_data] +_pytest = py.typed +pytest = py.typed + [build_sphinx] source-dir = doc/en/ build-dir = doc/build diff --git a/src/_pytest/py.typed b/src/_pytest/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pytest/py.typed b/src/pytest/py.typed new file mode 100644 index 00000000000..e69de29bb2d From b6a31b9c4d43333b5e8ca8f0a0935544e29a21a5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 6 Jul 2020 20:28:30 -0300 Subject: [PATCH 446/823] Remove warning about development/outdated docs Unfortunately couldn't figure out how to fix the generated link, so at least for now remove it to avoid confusion. Fix #7331 --- doc/en/_templates/layout.html | 52 +++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 doc/en/_templates/layout.html diff --git a/doc/en/_templates/layout.html b/doc/en/_templates/layout.html new file mode 100644 index 00000000000..f7096eaaa5e --- /dev/null +++ b/doc/en/_templates/layout.html @@ -0,0 +1,52 @@ +{# + + Copied from: + + https://raw.githubusercontent.com/pallets/pallets-sphinx-themes/b0c6c41849b4e15cbf62cc1d95c05ef2b3e155c8/src/pallets_sphinx_themes/themes/pocoo/layout.html + + And removed the warning version (see #7331). + +#} + +{% extends "basic/layout.html" %} + +{% set metatags %} + {{- metatags }} + +{%- endset %} + +{% block extrahead %} + {%- if page_canonical_url %} + + {%- endif %} + + {{ super() }} +{%- endblock %} + +{% block sidebarlogo %} + {% if pagename != "index" or theme_index_sidebar_logo %} + {{ super() }} + {% endif %} +{% endblock %} + +{% block relbar2 %}{% endblock %} + +{% block sidebar2 %} + + {{- super() }} +{%- endblock %} + +{% block footer %} + {{ super() }} + {%- if READTHEDOCS and not readthedocs_docsearch %} + + {%- endif %} + {{ js_tag("_static/version_warning_offset.js") }} +{% endblock %} From 93d2ccbfb7b99755e8f4a2a3947c466949ee3c46 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 Jul 2020 07:39:35 -0300 Subject: [PATCH 447/823] Point to stable docs instead of latest Now that our master might contain new features, it is best to point users to the stable docs rather than the latest --- CHANGELOG.rst | 2 +- CONTRIBUTING.rst | 2 +- README.rst | 20 ++--- doc/en/announce/release-2.0.0.rst | 8 +- doc/en/announce/release-2.0.1.rst | 2 +- doc/en/announce/release-2.1.0.rst | 2 +- doc/en/announce/release-2.2.0.rst | 6 +- doc/en/announce/release-2.3.0.rst | 10 +-- doc/en/announce/release-2.3.4.rst | 2 +- doc/en/announce/release-2.4.0.rst | 2 +- doc/en/announce/release-2.7.0.rst | 2 +- doc/en/announce/release-2.9.0.rst | 2 +- doc/en/announce/release-3.0.1.rst | 2 +- doc/en/announce/release-3.0.2.rst | 2 +- doc/en/announce/release-3.0.3.rst | 2 +- doc/en/announce/release-3.0.4.rst | 2 +- doc/en/announce/release-3.0.5.rst | 2 +- doc/en/announce/release-3.0.6.rst | 2 +- doc/en/announce/release-3.0.7.rst | 2 +- doc/en/announce/release-3.1.0.rst | 2 +- doc/en/announce/release-3.1.1.rst | 2 +- doc/en/announce/release-3.1.2.rst | 2 +- doc/en/announce/release-3.1.3.rst | 2 +- doc/en/announce/release-3.10.0.rst | 4 +- doc/en/announce/release-3.10.1.rst | 2 +- doc/en/announce/release-3.2.0.rst | 2 +- doc/en/announce/release-3.2.1.rst | 2 +- doc/en/announce/release-3.2.2.rst | 2 +- doc/en/announce/release-3.2.3.rst | 2 +- doc/en/announce/release-3.2.4.rst | 2 +- doc/en/announce/release-3.2.5.rst | 2 +- doc/en/announce/release-3.3.0.rst | 2 +- doc/en/announce/release-3.3.1.rst | 2 +- doc/en/announce/release-3.3.2.rst | 2 +- doc/en/announce/release-3.4.0.rst | 2 +- doc/en/announce/release-3.4.1.rst | 2 +- doc/en/announce/release-3.4.2.rst | 2 +- doc/en/announce/release-3.5.0.rst | 2 +- doc/en/announce/release-3.5.1.rst | 2 +- doc/en/announce/release-3.6.0.rst | 2 +- doc/en/announce/release-3.6.1.rst | 2 +- doc/en/announce/release-3.6.2.rst | 2 +- doc/en/announce/release-3.6.3.rst | 2 +- doc/en/announce/release-3.6.4.rst | 2 +- doc/en/announce/release-3.7.0.rst | 2 +- doc/en/announce/release-3.7.1.rst | 2 +- doc/en/announce/release-3.7.2.rst | 2 +- doc/en/announce/release-3.7.3.rst | 2 +- doc/en/announce/release-3.7.4.rst | 2 +- doc/en/announce/release-3.8.0.rst | 4 +- doc/en/announce/release-3.8.1.rst | 2 +- doc/en/announce/release-3.8.2.rst | 2 +- doc/en/announce/release-3.9.0.rst | 4 +- doc/en/announce/release-3.9.1.rst | 2 +- doc/en/announce/release-3.9.2.rst | 2 +- doc/en/announce/release-3.9.3.rst | 2 +- doc/en/announce/release-4.0.0.rst | 4 +- doc/en/announce/release-4.0.1.rst | 2 +- doc/en/announce/release-4.0.2.rst | 2 +- doc/en/announce/release-4.1.0.rst | 4 +- doc/en/announce/release-4.1.1.rst | 2 +- doc/en/announce/release-4.2.0.rst | 4 +- doc/en/announce/release-4.2.1.rst | 2 +- doc/en/announce/release-4.3.0.rst | 4 +- doc/en/announce/release-4.3.1.rst | 2 +- doc/en/announce/release-4.4.0.rst | 4 +- doc/en/announce/release-4.4.1.rst | 2 +- doc/en/announce/release-4.4.2.rst | 2 +- doc/en/announce/release-4.5.0.rst | 4 +- doc/en/announce/release-4.6.0.rst | 4 +- doc/en/announce/release-4.6.1.rst | 2 +- doc/en/announce/release-4.6.2.rst | 2 +- doc/en/announce/release-4.6.3.rst | 2 +- doc/en/announce/release-4.6.4.rst | 2 +- doc/en/announce/release-4.6.5.rst | 2 +- doc/en/announce/release-4.6.6.rst | 2 +- doc/en/announce/release-4.6.7.rst | 2 +- doc/en/announce/release-4.6.8.rst | 2 +- doc/en/announce/release-4.6.9.rst | 2 +- doc/en/announce/release-5.0.0.rst | 4 +- doc/en/announce/release-5.0.1.rst | 2 +- doc/en/announce/release-5.1.0.rst | 4 +- doc/en/announce/release-5.1.1.rst | 2 +- doc/en/announce/release-5.1.2.rst | 2 +- doc/en/announce/release-5.1.3.rst | 2 +- doc/en/announce/release-5.2.0.rst | 4 +- doc/en/announce/release-5.2.1.rst | 2 +- doc/en/announce/release-5.2.2.rst | 2 +- doc/en/announce/release-5.2.3.rst | 2 +- doc/en/announce/release-5.2.4.rst | 2 +- doc/en/announce/release-5.3.0.rst | 4 +- doc/en/announce/release-5.3.1.rst | 2 +- doc/en/announce/release-5.3.2.rst | 2 +- doc/en/announce/release-5.3.3.rst | 2 +- doc/en/announce/release-5.3.4.rst | 2 +- doc/en/announce/release-5.3.5.rst | 2 +- doc/en/announce/release-5.4.0.rst | 4 +- doc/en/announce/release-5.4.1.rst | 2 +- doc/en/announce/release-5.4.2.rst | 2 +- doc/en/announce/release-5.4.3.rst | 2 +- doc/en/assert.rst | 2 +- doc/en/changelog.rst | 114 ++++++++++++++--------------- doc/en/deprecations.rst | 2 +- doc/en/example/markers.rst | 20 ++--- doc/en/flaky.rst | 2 +- doc/en/funcarg_compare.rst | 2 +- doc/en/getting-started.rst | 2 +- doc/en/usage.rst | 2 +- doc/en/warnings.rst | 4 +- doc/en/writing_plugins.rst | 4 +- src/_pytest/cacheprovider.py | 2 +- src/_pytest/config/__init__.py | 2 +- src/_pytest/deprecated.py | 8 +- src/_pytest/fixtures.py | 4 +- src/_pytest/mark/structures.py | 2 +- src/_pytest/python.py | 4 +- src/_pytest/terminal.py | 2 +- src/_pytest/warning_types.py | 2 +- src/_pytest/warnings.py | 2 +- testing/deprecated_test.py | 2 +- testing/test_pluginmanager.py | 2 +- 121 files changed, 226 insertions(+), 226 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 49649f7894f..3865f250c26 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,6 @@ Changelog ========= -The pytest CHANGELOG is located `here `__. +The pytest CHANGELOG is located `here `__. The source document can be found at: https://github.com/pytest-dev/pytest/blob/master/doc/en/changelog.rst diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9ff854ffaf7..0523c0ece77 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -294,7 +294,7 @@ Here is a simple overview, with pytest-specific bits: Writing Tests ~~~~~~~~~~~~~ -Writing tests for plugins or for pytest itself is often done using the `testdir fixture `_, as a "black-box" test. +Writing tests for plugins or for pytest itself is often done using the `testdir fixture `_, as a "black-box" test. For example, to ensure a simple test passes you can write: diff --git a/README.rst b/README.rst index 0c05c9e33a7..00c85ae37ce 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -.. image:: https://docs.pytest.org/en/latest/_static/pytest1.png - :target: https://docs.pytest.org/en/latest/ +.. image:: https://docs.pytest.org/en/stable/_static/pytest1.png + :target: https://docs.pytest.org/en/stable/ :align: center :alt: pytest @@ -71,23 +71,23 @@ To execute it:: ========================== 1 failed in 0.04 seconds =========================== -Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See `getting-started `_ for more examples. +Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See `getting-started `_ for more examples. Features -------- -- Detailed info on failing `assert statements `_ (no need to remember ``self.assert*`` names); +- Detailed info on failing `assert statements `_ (no need to remember ``self.assert*`` names); - `Auto-discovery - `_ + `_ of test modules and functions; -- `Modular fixtures `_ for +- `Modular fixtures `_ for managing small or parametrized long-lived test resources; -- Can run `unittest `_ (or trial), - `nose `_ test suites out of the box; +- Can run `unittest `_ (or trial), + `nose `_ test suites out of the box; - Python 3.5+ and PyPy3; @@ -97,7 +97,7 @@ Features Documentation ------------- -For full documentation, including installation, tutorials and PDF documents, please see https://docs.pytest.org/en/latest/. +For full documentation, including installation, tutorials and PDF documents, please see https://docs.pytest.org/en/stable/. Bugs/Requests @@ -109,7 +109,7 @@ Please use the `GitHub issue tracker `__ page for fixes and enhancements of each version. +Consult the `Changelog `__ page for fixes and enhancements of each version. Support pytest diff --git a/doc/en/announce/release-2.0.0.rst b/doc/en/announce/release-2.0.0.rst index d9d90c09a42..1aaad740a4f 100644 --- a/doc/en/announce/release-2.0.0.rst +++ b/doc/en/announce/release-2.0.0.rst @@ -7,7 +7,7 @@ see below for summary and detailed lists. A lot of long-deprecated code has been removed, resulting in a much smaller and cleaner implementation. See the new docs with examples here: - http://pytest.org/en/latest/index.html + http://pytest.org/en/stable/index.html A note on packaging: pytest used to part of the "py" distribution up until version py-1.3.4 but this has changed now: pytest-2.0.0 only @@ -36,12 +36,12 @@ New Features import pytest ; pytest.main(arglist, pluginlist) - see http://pytest.org/en/latest/usage.html for details. + see http://pytest.org/en/stable/usage.html for details. - new and better reporting information in assert expressions if comparing lists, sequences or strings. - see http://pytest.org/en/latest/assert.html#newreport + see http://pytest.org/en/stable/assert.html#newreport - new configuration through ini-files (setup.cfg or tox.ini recognized), for example:: @@ -50,7 +50,7 @@ New Features norecursedirs = .hg data* # don't ever recurse in such dirs addopts = -x --pyargs # add these command line options by default - see http://pytest.org/en/latest/customize.html + see http://pytest.org/en/stable/customize.html - improved standard unittest support. In general py.test should now better be able to run custom unittest.TestCases like twisted trial diff --git a/doc/en/announce/release-2.0.1.rst b/doc/en/announce/release-2.0.1.rst index f86537e1d01..72401d8098f 100644 --- a/doc/en/announce/release-2.0.1.rst +++ b/doc/en/announce/release-2.0.1.rst @@ -57,7 +57,7 @@ Changes between 2.0.0 and 2.0.1 - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output - introduce a mechanism to prevent/unregister plugins from the - command line, see http://pytest.org/en/latest/plugins.html#cmdunregister + command line, see http://pytest.org/en/stable/plugins.html#cmdunregister - activate resultlog plugin by default - fix regression wrt yielded tests which due to the collection-before-running semantics were not diff --git a/doc/en/announce/release-2.1.0.rst b/doc/en/announce/release-2.1.0.rst index 2a2181d9754..29463ab533e 100644 --- a/doc/en/announce/release-2.1.0.rst +++ b/doc/en/announce/release-2.1.0.rst @@ -12,7 +12,7 @@ courtesy of Benjamin Peterson. You can now safely use ``assert`` statements in test modules without having to worry about side effects or python optimization ("-OO") options. This is achieved by rewriting assert statements in test modules upon import, using a PEP302 hook. -See https://docs.pytest.org/en/latest/assert.html for +See https://docs.pytest.org/en/stable/assert.html for detailed information. The work has been partly sponsored by my company, merlinux GmbH. diff --git a/doc/en/announce/release-2.2.0.rst b/doc/en/announce/release-2.2.0.rst index 79e4dfd1590..0193ffb3465 100644 --- a/doc/en/announce/release-2.2.0.rst +++ b/doc/en/announce/release-2.2.0.rst @@ -9,7 +9,7 @@ with these improvements: - new @pytest.mark.parametrize decorator to run tests with different arguments - new metafunc.parametrize() API for parametrizing arguments independently - - see examples at http://pytest.org/en/latest/example/parametrize.html + - see examples at http://pytest.org/en/stable/example/parametrize.html - NOTE that parametrize() related APIs are still a bit experimental and might change in future releases. @@ -18,7 +18,7 @@ with these improvements: - "-m markexpr" option for selecting tests according to their mark - a new "markers" ini-variable for registering test markers for your project - the new "--strict" bails out with an error if using unregistered markers. - - see examples at http://pytest.org/en/latest/example/markers.html + - see examples at http://pytest.org/en/stable/example/markers.html * duration profiling: new "--duration=N" option showing the N slowest test execution or setup/teardown calls. This is most useful if you want to @@ -78,7 +78,7 @@ Changes between 2.1.3 and 2.2.0 or through plugin hooks. Also introduce a "--strict" option which will treat unregistered markers as errors allowing to avoid typos and maintain a well described set of markers - for your test suite. See examples at http://pytest.org/en/latest/mark.html + for your test suite. See examples at http://pytest.org/en/stable/mark.html and its links. - issue50: introduce "-m marker" option to select tests based on markers (this is a stricter and more predictable version of "-k" in that "-m" diff --git a/doc/en/announce/release-2.3.0.rst b/doc/en/announce/release-2.3.0.rst index 1b9d0dcc1a5..d938192bb0d 100644 --- a/doc/en/announce/release-2.3.0.rst +++ b/doc/en/announce/release-2.3.0.rst @@ -13,12 +13,12 @@ re-usable fixture design. For detailed info and tutorial-style examples, see: - http://pytest.org/en/latest/fixture.html + http://pytest.org/en/stable/fixture.html Moreover, there is now support for using pytest fixtures/funcargs with unittest-style suites, see here for examples: - http://pytest.org/en/latest/unittest.html + http://pytest.org/en/stable/unittest.html Besides, more unittest-test suites are now expected to "simply work" with pytest. @@ -29,11 +29,11 @@ pytest-2.2.4. If you are interested in the precise reasoning (including examples) of the pytest-2.3 fixture evolution, please consult -http://pytest.org/en/latest/funcarg_compare.html +http://pytest.org/en/stable/funcarg_compare.html For general info on installation and getting started: - http://pytest.org/en/latest/getting-started.html + http://pytest.org/en/stable/getting-started.html Docs and PDF access as usual at: @@ -94,7 +94,7 @@ Changes between 2.2.4 and 2.3.0 - pluginmanager.register(...) now raises ValueError if the plugin has been already registered or the name is taken -- fix issue159: improve http://pytest.org/en/latest/faq.html +- fix issue159: improve http://pytest.org/en/stable/faq.html especially with respect to the "magic" history, also mention pytest-django, trial and unittest integration. diff --git a/doc/en/announce/release-2.3.4.rst b/doc/en/announce/release-2.3.4.rst index b00430f943f..26f76630e84 100644 --- a/doc/en/announce/release-2.3.4.rst +++ b/doc/en/announce/release-2.3.4.rst @@ -16,7 +16,7 @@ comes with the following fixes and features: - yielded test functions will now have autouse-fixtures active but cannot accept fixtures as funcargs - it's anyway recommended to rather use the post-2.0 parametrize features instead of yield, see: - http://pytest.org/en/latest/example/parametrize.html + http://pytest.org/en/stable/example/parametrize.html - fix autouse-issue where autouse-fixtures would not be discovered if defined in an a/conftest.py file and tests in a/tests/test_some.py - fix issue226 - LIFO ordering for fixture teardowns diff --git a/doc/en/announce/release-2.4.0.rst b/doc/en/announce/release-2.4.0.rst index 6cd14bc2dfc..68297b26c4e 100644 --- a/doc/en/announce/release-2.4.0.rst +++ b/doc/en/announce/release-2.4.0.rst @@ -7,7 +7,7 @@ from a few supposedly very minor incompatibilities. See below for a full list of details. A few feature highlights: - new yield-style fixtures `pytest.yield_fixture - `_, allowing to use + `_, allowing to use existing with-style context managers in fixture functions. - improved pdb support: ``import pdb ; pdb.set_trace()`` now works diff --git a/doc/en/announce/release-2.7.0.rst b/doc/en/announce/release-2.7.0.rst index cf798ff2c34..2f6d50d8b69 100644 --- a/doc/en/announce/release-2.7.0.rst +++ b/doc/en/announce/release-2.7.0.rst @@ -52,7 +52,7 @@ holger krekel - add ability to set command line options by environment variable PYTEST_ADDOPTS. - added documentation on the new pytest-dev teams on bitbucket and - github. See https://pytest.org/en/latest/contributing.html . + github. See https://pytest.org/en/stable/contributing.html . Thanks to Anatoly for pushing and initial work on this. - fix issue650: new option ``--docttest-ignore-import-errors`` which diff --git a/doc/en/announce/release-2.9.0.rst b/doc/en/announce/release-2.9.0.rst index 9e085669023..f5d4be71347 100644 --- a/doc/en/announce/release-2.9.0.rst +++ b/doc/en/announce/release-2.9.0.rst @@ -131,7 +131,7 @@ The py.test Development Team with same name. -.. _`traceback style docs`: https://pytest.org/en/latest/usage.html#modifying-python-traceback-printing +.. _`traceback style docs`: https://pytest.org/en/stable/usage.html#modifying-python-traceback-printing .. _#1422: https://github.com/pytest-dev/pytest/issues/1422 .. _#1379: https://github.com/pytest-dev/pytest/issues/1379 diff --git a/doc/en/announce/release-3.0.1.rst b/doc/en/announce/release-3.0.1.rst index eb6f6a50ef7..8f5cfe411aa 100644 --- a/doc/en/announce/release-3.0.1.rst +++ b/doc/en/announce/release-3.0.1.rst @@ -8,7 +8,7 @@ drop-in replacement. To upgrade: pip install --upgrade pytest -The changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.0.2.rst b/doc/en/announce/release-3.0.2.rst index 4af412fc5ee..86ba82ca6e6 100644 --- a/doc/en/announce/release-3.0.2.rst +++ b/doc/en/announce/release-3.0.2.rst @@ -8,7 +8,7 @@ drop-in replacement. To upgrade:: pip install --upgrade pytest -The changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.0.3.rst b/doc/en/announce/release-3.0.3.rst index 896d4787304..89a2e0c744e 100644 --- a/doc/en/announce/release-3.0.3.rst +++ b/doc/en/announce/release-3.0.3.rst @@ -8,7 +8,7 @@ being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.0.4.rst b/doc/en/announce/release-3.0.4.rst index 855bc56d5b8..72c2d29464d 100644 --- a/doc/en/announce/release-3.0.4.rst +++ b/doc/en/announce/release-3.0.4.rst @@ -8,7 +8,7 @@ being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.0.5.rst b/doc/en/announce/release-3.0.5.rst index 2f369827588..97edb7d4628 100644 --- a/doc/en/announce/release-3.0.5.rst +++ b/doc/en/announce/release-3.0.5.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.0.6.rst b/doc/en/announce/release-3.0.6.rst index 149c2d65e1a..9c072cedcca 100644 --- a/doc/en/announce/release-3.0.6.rst +++ b/doc/en/announce/release-3.0.6.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.0.7.rst b/doc/en/announce/release-3.0.7.rst index b37e4f61dee..4b7e075e76a 100644 --- a/doc/en/announce/release-3.0.7.rst +++ b/doc/en/announce/release-3.0.7.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.1.0.rst b/doc/en/announce/release-3.1.0.rst index 99cc6bdbe20..b84fd4c3cf9 100644 --- a/doc/en/announce/release-3.1.0.rst +++ b/doc/en/announce/release-3.1.0.rst @@ -9,7 +9,7 @@ against itself, passing on many different interpreters and platforms. This release contains a bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: -http://doc.pytest.org/en/latest/changelog.html +http://doc.pytest.org/en/stable/changelog.html For complete documentation, please visit: diff --git a/doc/en/announce/release-3.1.1.rst b/doc/en/announce/release-3.1.1.rst index 4ce7531977c..135b2fe8443 100644 --- a/doc/en/announce/release-3.1.1.rst +++ b/doc/en/announce/release-3.1.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.1.2.rst b/doc/en/announce/release-3.1.2.rst index 8ed0c93e9ad..a9b85c4715c 100644 --- a/doc/en/announce/release-3.1.2.rst +++ b/doc/en/announce/release-3.1.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.1.3.rst b/doc/en/announce/release-3.1.3.rst index d7771f92232..bc2b85fcfd5 100644 --- a/doc/en/announce/release-3.1.3.rst +++ b/doc/en/announce/release-3.1.3.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.10.0.rst b/doc/en/announce/release-3.10.0.rst index b53df270219..c16c381e8d0 100644 --- a/doc/en/announce/release-3.10.0.rst +++ b/doc/en/announce/release-3.10.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-3.10.1.rst b/doc/en/announce/release-3.10.1.rst index 556b24ae15b..ad365f63474 100644 --- a/doc/en/announce/release-3.10.1.rst +++ b/doc/en/announce/release-3.10.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.2.0.rst b/doc/en/announce/release-3.2.0.rst index 4d2830edd2d..4d5d6f1671f 100644 --- a/doc/en/announce/release-3.2.0.rst +++ b/doc/en/announce/release-3.2.0.rst @@ -9,7 +9,7 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - http://doc.pytest.org/en/latest/changelog.html + http://doc.pytest.org/en/stable/changelog.html For complete documentation, please visit: diff --git a/doc/en/announce/release-3.2.1.rst b/doc/en/announce/release-3.2.1.rst index afe2c5bfe2c..c40217d311d 100644 --- a/doc/en/announce/release-3.2.1.rst +++ b/doc/en/announce/release-3.2.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.2.2.rst b/doc/en/announce/release-3.2.2.rst index 88e32873a1b..5e6c43ab177 100644 --- a/doc/en/announce/release-3.2.2.rst +++ b/doc/en/announce/release-3.2.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.2.3.rst b/doc/en/announce/release-3.2.3.rst index ddfda4d132f..50dce29c1ad 100644 --- a/doc/en/announce/release-3.2.3.rst +++ b/doc/en/announce/release-3.2.3.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.2.4.rst b/doc/en/announce/release-3.2.4.rst index 65e486b7aa2..ff0b35781b1 100644 --- a/doc/en/announce/release-3.2.4.rst +++ b/doc/en/announce/release-3.2.4.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.2.5.rst b/doc/en/announce/release-3.2.5.rst index 2e5304c6f27..68caccbdbc5 100644 --- a/doc/en/announce/release-3.2.5.rst +++ b/doc/en/announce/release-3.2.5.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.3.0.rst b/doc/en/announce/release-3.3.0.rst index e0740e7d592..e57bbac6a91 100644 --- a/doc/en/announce/release-3.3.0.rst +++ b/doc/en/announce/release-3.3.0.rst @@ -9,7 +9,7 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - http://doc.pytest.org/en/latest/changelog.html + http://doc.pytest.org/en/stable/changelog.html For complete documentation, please visit: diff --git a/doc/en/announce/release-3.3.1.rst b/doc/en/announce/release-3.3.1.rst index 7eed836ae6d..98b6fa6c1ba 100644 --- a/doc/en/announce/release-3.3.1.rst +++ b/doc/en/announce/release-3.3.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.3.2.rst b/doc/en/announce/release-3.3.2.rst index d9acef947dd..7a2577d1ff8 100644 --- a/doc/en/announce/release-3.3.2.rst +++ b/doc/en/announce/release-3.3.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.4.0.rst b/doc/en/announce/release-3.4.0.rst index df1e004f1cc..ec6725370cd 100644 --- a/doc/en/announce/release-3.4.0.rst +++ b/doc/en/announce/release-3.4.0.rst @@ -9,7 +9,7 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - http://doc.pytest.org/en/latest/changelog.html + http://doc.pytest.org/en/stable/changelog.html For complete documentation, please visit: diff --git a/doc/en/announce/release-3.4.1.rst b/doc/en/announce/release-3.4.1.rst index e37f5d7e240..d83949453a2 100644 --- a/doc/en/announce/release-3.4.1.rst +++ b/doc/en/announce/release-3.4.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.4.2.rst b/doc/en/announce/release-3.4.2.rst index 8e9988228fa..07cd9d3a8ba 100644 --- a/doc/en/announce/release-3.4.2.rst +++ b/doc/en/announce/release-3.4.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.5.0.rst b/doc/en/announce/release-3.5.0.rst index 54a05cea24d..ef64dc381e6 100644 --- a/doc/en/announce/release-3.5.0.rst +++ b/doc/en/announce/release-3.5.0.rst @@ -9,7 +9,7 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - http://doc.pytest.org/en/latest/changelog.html + http://doc.pytest.org/en/stable/changelog.html For complete documentation, please visit: diff --git a/doc/en/announce/release-3.5.1.rst b/doc/en/announce/release-3.5.1.rst index 91f14390eeb..802be036848 100644 --- a/doc/en/announce/release-3.5.1.rst +++ b/doc/en/announce/release-3.5.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.6.0.rst b/doc/en/announce/release-3.6.0.rst index 37361cf4add..38a8b9e3f09 100644 --- a/doc/en/announce/release-3.6.0.rst +++ b/doc/en/announce/release-3.6.0.rst @@ -9,7 +9,7 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - http://doc.pytest.org/en/latest/changelog.html + http://doc.pytest.org/en/stable/changelog.html For complete documentation, please visit: diff --git a/doc/en/announce/release-3.6.1.rst b/doc/en/announce/release-3.6.1.rst index 3bedcf46a85..d971a3d4907 100644 --- a/doc/en/announce/release-3.6.1.rst +++ b/doc/en/announce/release-3.6.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.6.2.rst b/doc/en/announce/release-3.6.2.rst index a1215f57689..9d919957939 100644 --- a/doc/en/announce/release-3.6.2.rst +++ b/doc/en/announce/release-3.6.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.6.3.rst b/doc/en/announce/release-3.6.3.rst index 07bb05a3d72..4dda2460dac 100644 --- a/doc/en/announce/release-3.6.3.rst +++ b/doc/en/announce/release-3.6.3.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.6.4.rst b/doc/en/announce/release-3.6.4.rst index fd6cff50305..2c0f9efeccf 100644 --- a/doc/en/announce/release-3.6.4.rst +++ b/doc/en/announce/release-3.6.4.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.7.0.rst b/doc/en/announce/release-3.7.0.rst index 922b22517e9..ef6c44f0aab 100644 --- a/doc/en/announce/release-3.7.0.rst +++ b/doc/en/announce/release-3.7.0.rst @@ -9,7 +9,7 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - http://doc.pytest.org/en/latest/changelog.html + http://doc.pytest.org/en/stable/changelog.html For complete documentation, please visit: diff --git a/doc/en/announce/release-3.7.1.rst b/doc/en/announce/release-3.7.1.rst index e1c35123dbf..7da5a3e1f7d 100644 --- a/doc/en/announce/release-3.7.1.rst +++ b/doc/en/announce/release-3.7.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.7.2.rst b/doc/en/announce/release-3.7.2.rst index 4f7e0744d50..fcc6121752d 100644 --- a/doc/en/announce/release-3.7.2.rst +++ b/doc/en/announce/release-3.7.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.7.3.rst b/doc/en/announce/release-3.7.3.rst index 454d4fdfee7..ee87da60d23 100644 --- a/doc/en/announce/release-3.7.3.rst +++ b/doc/en/announce/release-3.7.3.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at http://doc.pytest.org/en/latest/changelog.html. +The full changelog is available at http://doc.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.7.4.rst b/doc/en/announce/release-3.7.4.rst index 0ab8938f4f6..45be4293885 100644 --- a/doc/en/announce/release-3.7.4.rst +++ b/doc/en/announce/release-3.7.4.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.8.0.rst b/doc/en/announce/release-3.8.0.rst index 1fc344ea23e..5369ffc7dba 100644 --- a/doc/en/announce/release-3.8.0.rst +++ b/doc/en/announce/release-3.8.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-3.8.1.rst b/doc/en/announce/release-3.8.1.rst index 3e05e58cb3f..f8f8accc4c9 100644 --- a/doc/en/announce/release-3.8.1.rst +++ b/doc/en/announce/release-3.8.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.8.2.rst b/doc/en/announce/release-3.8.2.rst index ecc47fbb33b..9ea94c98a21 100644 --- a/doc/en/announce/release-3.8.2.rst +++ b/doc/en/announce/release-3.8.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.9.0.rst b/doc/en/announce/release-3.9.0.rst index 14cfbe9037d..1d889e5bc85 100644 --- a/doc/en/announce/release-3.9.0.rst +++ b/doc/en/announce/release-3.9.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-3.9.1.rst b/doc/en/announce/release-3.9.1.rst index f050e465305..e1afb3759d2 100644 --- a/doc/en/announce/release-3.9.1.rst +++ b/doc/en/announce/release-3.9.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.9.2.rst b/doc/en/announce/release-3.9.2.rst index 1440831cb93..63e94e5aabb 100644 --- a/doc/en/announce/release-3.9.2.rst +++ b/doc/en/announce/release-3.9.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-3.9.3.rst b/doc/en/announce/release-3.9.3.rst index 8d84b4cabcb..661ddb5cb54 100644 --- a/doc/en/announce/release-3.9.3.rst +++ b/doc/en/announce/release-3.9.3.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.0.0.rst b/doc/en/announce/release-4.0.0.rst index e5ad69b5fd6..b91fd59542d 100644 --- a/doc/en/announce/release-4.0.0.rst +++ b/doc/en/announce/release-4.0.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-4.0.1.rst b/doc/en/announce/release-4.0.1.rst index 31b222c03b5..2902a6db9fb 100644 --- a/doc/en/announce/release-4.0.1.rst +++ b/doc/en/announce/release-4.0.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.0.2.rst b/doc/en/announce/release-4.0.2.rst index 3b6e4be7183..f439b88fe2c 100644 --- a/doc/en/announce/release-4.0.2.rst +++ b/doc/en/announce/release-4.0.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.1.0.rst b/doc/en/announce/release-4.1.0.rst index b7a076f61c9..77aaf74af65 100644 --- a/doc/en/announce/release-4.1.0.rst +++ b/doc/en/announce/release-4.1.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-4.1.1.rst b/doc/en/announce/release-4.1.1.rst index 80644fc84ef..1f45e082f89 100644 --- a/doc/en/announce/release-4.1.1.rst +++ b/doc/en/announce/release-4.1.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.2.0.rst b/doc/en/announce/release-4.2.0.rst index 6c262c1e01b..11520acf331 100644 --- a/doc/en/announce/release-4.2.0.rst +++ b/doc/en/announce/release-4.2.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-4.2.1.rst b/doc/en/announce/release-4.2.1.rst index 5aec022df0b..36beafe11d2 100644 --- a/doc/en/announce/release-4.2.1.rst +++ b/doc/en/announce/release-4.2.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.3.0.rst b/doc/en/announce/release-4.3.0.rst index 59393814846..9dcbe415eec 100644 --- a/doc/en/announce/release-4.3.0.rst +++ b/doc/en/announce/release-4.3.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-4.3.1.rst b/doc/en/announce/release-4.3.1.rst index 54cf8b3fcd8..4251c744e55 100644 --- a/doc/en/announce/release-4.3.1.rst +++ b/doc/en/announce/release-4.3.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.4.0.rst b/doc/en/announce/release-4.4.0.rst index 4c5bcbc7d35..d4abfac22a7 100644 --- a/doc/en/announce/release-4.4.0.rst +++ b/doc/en/announce/release-4.4.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-4.4.1.rst b/doc/en/announce/release-4.4.1.rst index 12c0ee7798b..1272cd8fde1 100644 --- a/doc/en/announce/release-4.4.1.rst +++ b/doc/en/announce/release-4.4.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.4.2.rst b/doc/en/announce/release-4.4.2.rst index 4fe2dac56b3..5876e83b3b6 100644 --- a/doc/en/announce/release-4.4.2.rst +++ b/doc/en/announce/release-4.4.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.5.0.rst b/doc/en/announce/release-4.5.0.rst index 37c16cd7224..957e2c17285 100644 --- a/doc/en/announce/release-4.5.0.rst +++ b/doc/en/announce/release-4.5.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-4.6.0.rst b/doc/en/announce/release-4.6.0.rst index 373f5d66eb7..7c7cf29d26a 100644 --- a/doc/en/announce/release-4.6.0.rst +++ b/doc/en/announce/release-4.6.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-4.6.1.rst b/doc/en/announce/release-4.6.1.rst index 78d017544d2..c79839b7b52 100644 --- a/doc/en/announce/release-4.6.1.rst +++ b/doc/en/announce/release-4.6.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.6.2.rst b/doc/en/announce/release-4.6.2.rst index 8526579b9e7..cfc595293ae 100644 --- a/doc/en/announce/release-4.6.2.rst +++ b/doc/en/announce/release-4.6.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.6.3.rst b/doc/en/announce/release-4.6.3.rst index 0bfb355a15a..f578464a7a3 100644 --- a/doc/en/announce/release-4.6.3.rst +++ b/doc/en/announce/release-4.6.3.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.6.4.rst b/doc/en/announce/release-4.6.4.rst index 7b35ed4f0d4..0eefcbeb1c2 100644 --- a/doc/en/announce/release-4.6.4.rst +++ b/doc/en/announce/release-4.6.4.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.6.5.rst b/doc/en/announce/release-4.6.5.rst index 6998d4e4c5f..1ebf361fdf9 100644 --- a/doc/en/announce/release-4.6.5.rst +++ b/doc/en/announce/release-4.6.5.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.6.6.rst b/doc/en/announce/release-4.6.6.rst index c47a31695b2..b3bf1e431c7 100644 --- a/doc/en/announce/release-4.6.6.rst +++ b/doc/en/announce/release-4.6.6.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.6.7.rst b/doc/en/announce/release-4.6.7.rst index 0e6cf6a950a..f9d01845ec2 100644 --- a/doc/en/announce/release-4.6.7.rst +++ b/doc/en/announce/release-4.6.7.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.6.8.rst b/doc/en/announce/release-4.6.8.rst index 3c04e5dbe9b..5cabe7826e9 100644 --- a/doc/en/announce/release-4.6.8.rst +++ b/doc/en/announce/release-4.6.8.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-4.6.9.rst b/doc/en/announce/release-4.6.9.rst index ae0478c52d9..7f7bb5996ea 100644 --- a/doc/en/announce/release-4.6.9.rst +++ b/doc/en/announce/release-4.6.9.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.0.0.rst b/doc/en/announce/release-5.0.0.rst index ca516060215..99c92a505fe 100644 --- a/doc/en/announce/release-5.0.0.rst +++ b/doc/en/announce/release-5.0.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-5.0.1.rst b/doc/en/announce/release-5.0.1.rst index 541aeb49109..e16a8f716f1 100644 --- a/doc/en/announce/release-5.0.1.rst +++ b/doc/en/announce/release-5.0.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.1.0.rst b/doc/en/announce/release-5.1.0.rst index 73e956d77e3..4293c581312 100644 --- a/doc/en/announce/release-5.1.0.rst +++ b/doc/en/announce/release-5.1.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-5.1.1.rst b/doc/en/announce/release-5.1.1.rst index 9cb731ebb98..bb8de48014a 100644 --- a/doc/en/announce/release-5.1.1.rst +++ b/doc/en/announce/release-5.1.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.1.2.rst b/doc/en/announce/release-5.1.2.rst index ac6e005819b..c4cb8e3fb44 100644 --- a/doc/en/announce/release-5.1.2.rst +++ b/doc/en/announce/release-5.1.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.1.3.rst b/doc/en/announce/release-5.1.3.rst index 882b79bde2e..c4e88aed28e 100644 --- a/doc/en/announce/release-5.1.3.rst +++ b/doc/en/announce/release-5.1.3.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.2.0.rst b/doc/en/announce/release-5.2.0.rst index 8eae6dd734d..fbe27031b1e 100644 --- a/doc/en/announce/release-5.2.0.rst +++ b/doc/en/announce/release-5.2.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-5.2.1.rst b/doc/en/announce/release-5.2.1.rst index 312cfd778e6..fe42b9bf15f 100644 --- a/doc/en/announce/release-5.2.1.rst +++ b/doc/en/announce/release-5.2.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.2.2.rst b/doc/en/announce/release-5.2.2.rst index 8a2ced9eb6e..89fd6a534d4 100644 --- a/doc/en/announce/release-5.2.2.rst +++ b/doc/en/announce/release-5.2.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.2.3.rst b/doc/en/announce/release-5.2.3.rst index bfb62a1b8d9..bab174495d9 100644 --- a/doc/en/announce/release-5.2.3.rst +++ b/doc/en/announce/release-5.2.3.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.2.4.rst b/doc/en/announce/release-5.2.4.rst index 05677e77f55..5f518967975 100644 --- a/doc/en/announce/release-5.2.4.rst +++ b/doc/en/announce/release-5.2.4.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.3.0.rst b/doc/en/announce/release-5.3.0.rst index 9855a7a2d07..54c33e192b0 100644 --- a/doc/en/announce/release-5.3.0.rst +++ b/doc/en/announce/release-5.3.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from pypi via: diff --git a/doc/en/announce/release-5.3.1.rst b/doc/en/announce/release-5.3.1.rst index acf13bf6d8d..d575bb70e3f 100644 --- a/doc/en/announce/release-5.3.1.rst +++ b/doc/en/announce/release-5.3.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.3.2.rst b/doc/en/announce/release-5.3.2.rst index dbd657da3c3..d562a33fb0f 100644 --- a/doc/en/announce/release-5.3.2.rst +++ b/doc/en/announce/release-5.3.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.3.3.rst b/doc/en/announce/release-5.3.3.rst index 39820f3bcd0..40a6fb5b560 100644 --- a/doc/en/announce/release-5.3.3.rst +++ b/doc/en/announce/release-5.3.3.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.3.4.rst b/doc/en/announce/release-5.3.4.rst index 75bf4e6f34e..0750a9d404e 100644 --- a/doc/en/announce/release-5.3.4.rst +++ b/doc/en/announce/release-5.3.4.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.3.5.rst b/doc/en/announce/release-5.3.5.rst index 46095339f55..e632ce85388 100644 --- a/doc/en/announce/release-5.3.5.rst +++ b/doc/en/announce/release-5.3.5.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.4.0.rst b/doc/en/announce/release-5.4.0.rst index a89c3ca6bc7..cb91e26ba36 100644 --- a/doc/en/announce/release-5.4.0.rst +++ b/doc/en/announce/release-5.4.0.rst @@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms. This release contains a number of bug fixes and improvements, so users are encouraged to take a look at the CHANGELOG: - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from PyPI via: diff --git a/doc/en/announce/release-5.4.1.rst b/doc/en/announce/release-5.4.1.rst index 413bf7d2bac..f6a64efa492 100644 --- a/doc/en/announce/release-5.4.1.rst +++ b/doc/en/announce/release-5.4.1.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.4.2.rst b/doc/en/announce/release-5.4.2.rst index 233faf127b5..d742dd4aad4 100644 --- a/doc/en/announce/release-5.4.2.rst +++ b/doc/en/announce/release-5.4.2.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/announce/release-5.4.3.rst b/doc/en/announce/release-5.4.3.rst index 4d48fc1193f..6c995c16339 100644 --- a/doc/en/announce/release-5.4.3.rst +++ b/doc/en/announce/release-5.4.3.rst @@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. Thanks to all who contributed to this release, among them: diff --git a/doc/en/assert.rst b/doc/en/assert.rst index a3a34a9c653..87c79720f19 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -303,7 +303,7 @@ modules directly discovered by its test collection process, so **asserts in supporting modules which are not themselves test modules will not be rewritten**. You can manually enable assertion rewriting for an imported module by calling -`register_assert_rewrite `_ +`register_assert_rewrite `_ before you import it (a good place to do that is in your root ``conftest.py``). For further information, Benjamin Peterson wrote up `Behind the scenes of pytest's new assertion rewriting `_. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 40b5ee69014..ab622ec1e4f 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -433,7 +433,7 @@ Deprecations In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option is given in the command line but :confval:`junit_family` is not explicitly configured in ``pytest.ini``. - For more information, `see the docs `__. + For more information, `see the docs `__. @@ -700,7 +700,7 @@ Features - `#1682 `_: The ``scope`` parameter of ``@pytest.fixture`` can now be a callable that receives the fixture name and the ``config`` object as keyword-only parameters. - See `the docs `__ for more information. + See `the docs `__ for more information. - `#5764 `_: New behavior of the ``--pastebin`` option: failures to connect to the pastebin server are reported, without failing the pytest run @@ -805,7 +805,7 @@ Removals For more information consult - `Deprecations and Removals `__ in the docs. + `Deprecations and Removals `__ in the docs. - `#5565 `_: Removed unused support code for `unittest2 `__. @@ -839,7 +839,7 @@ Features - `#5564 `_: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. -- `#5576 `_: New `NUMBER `__ +- `#5576 `_: New `NUMBER `__ option for doctests to ignore irrelevant differences in floating-point numbers. Inspired by Sébastien Boisgérault's `numtest `__ extension for doctest. @@ -959,7 +959,7 @@ Important This release is a Python3.5+ only release. -For more details, see our `Python 2.7 and 3.4 support plan `__. +For more details, see our `Python 2.7 and 3.4 support plan `__. Removals -------- @@ -980,7 +980,7 @@ Removals instead of warning messages. **The affected features will be effectively removed in pytest 5.1**, so please consult the - `Deprecations and Removals `__ + `Deprecations and Removals `__ section in the docs for directions on how to update existing code. In the pytest ``5.0.X`` series, it is possible to change the errors back into warnings as a stop @@ -1036,7 +1036,7 @@ Deprecations Features -------- -- `#3457 `_: New `pytest_assertion_pass `__ +- `#3457 `_: New `pytest_assertion_pass `__ hook, called with context information when an assertion *passes*. This hook is still **experimental** so use it with caution. @@ -1049,7 +1049,7 @@ Features `pytest-faulthandler `__ plugin into the core, so users should remove that plugin from their requirements if used. - For more information see the docs: https://docs.pytest.org/en/latest/usage.html#fault-handler + For more information see the docs: https://docs.pytest.org/en/stable/usage.html#fault-handler - `#5452 `_: When warnings are configured as errors, pytest warnings now appear as originating from ``pytest.`` instead of the internal ``_pytest.warning_types.`` module. @@ -1318,7 +1318,7 @@ Important The ``4.6.X`` series will be the last series to support **Python 2 and Python 3.4**. -For more details, see our `Python 2.7 and 3.4 support plan `__. +For more details, see our `Python 2.7 and 3.4 support plan `__. Features @@ -1408,7 +1408,7 @@ Features The existing ``--strict`` option has the same behavior currently, but can be augmented in the future for additional checks. - .. _`markers option`: https://docs.pytest.org/en/latest/reference.html#confval-markers + .. _`markers option`: https://docs.pytest.org/en/stable/reference.html#confval-markers - `#5026 `_: Assertion failure messages for sequences and dicts contain the number of different items now. @@ -1465,7 +1465,7 @@ Features CRITICAL root:test_log_cli_enabled_disabled.py:3 critical message logged by test - The formatting can be changed through the `log_format `__ configuration option. + The formatting can be changed through the `log_format `__ configuration option. - `#5220 `_: ``--fixtures`` now also shows fixture scope for scopes other than ``"function"``. @@ -1601,7 +1601,7 @@ Features .. _pdb++: https://pypi.org/project/pdbpp/ -- `#4875 `_: The `testpaths `__ configuration option is now displayed next +- `#4875 `_: The `testpaths `__ configuration option is now displayed next to the ``rootdir`` and ``inifile`` lines in the pytest header if the option is in effect, i.e., directories or file names were not explicitly passed in the command line. @@ -1856,7 +1856,7 @@ pytest 4.2.0 (2019-01-30) Features -------- -- `#3094 `_: `Classic xunit-style `__ functions and methods +- `#3094 `_: `Classic xunit-style `__ functions and methods now obey the scope of *autouse* fixtures. This fixes a number of surprising issues like ``setup_method`` being called before session-scoped @@ -1974,27 +1974,27 @@ Removals - `#3078 `_: Remove legacy internal warnings system: ``config.warn``, ``Node.warn``. The ``pytest_logwarning`` now issues a warning when implemented. - See our `docs `__ on information on how to update your code. + See our `docs `__ on information on how to update your code. - `#3079 `_: Removed support for yield tests - they are fundamentally broken because they don't support fixtures properly since collection and test execution were separated. - See our `docs `__ on information on how to update your code. + See our `docs `__ on information on how to update your code. - `#3082 `_: Removed support for applying marks directly to values in ``@pytest.mark.parametrize``. Use ``pytest.param`` instead. - See our `docs `__ on information on how to update your code. + See our `docs `__ on information on how to update your code. - `#3083 `_: Removed ``Metafunc.addcall``. This was the predecessor mechanism to ``@pytest.mark.parametrize``. - See our `docs `__ on information on how to update your code. + See our `docs `__ on information on how to update your code. - `#3085 `_: Removed support for passing strings to ``pytest.main``. Now, always pass a list of strings instead. - See our `docs `__ on information on how to update your code. + See our `docs `__ on information on how to update your code. - `#3086 `_: ``[pytest]`` section in **setup.cfg** files is no longer supported, use ``[tool:pytest]`` instead. ``setup.cfg`` files @@ -2005,17 +2005,17 @@ Removals - `#3616 `_: Removed the deprecated compat properties for ``node.Class/Function/Module`` - use ``pytest.Class/Function/Module`` now. - See our `docs `__ on information on how to update your code. + See our `docs `__ on information on how to update your code. - `#4421 `_: Removed the implementation of the ``pytest_namespace`` hook. - See our `docs `__ on information on how to update your code. + See our `docs `__ on information on how to update your code. - `#4489 `_: Removed ``request.cached_setup``. This was the predecessor mechanism to modern fixtures. - See our `docs `__ on information on how to update your code. + See our `docs `__ on information on how to update your code. - `#4535 `_: Removed the deprecated ``PyCollector.makeitem`` method. This method was made public by mistake a long time ago. @@ -2023,12 +2023,12 @@ Removals - `#4543 `_: Removed support to define fixtures using the ``pytest_funcarg__`` prefix. Use the ``@pytest.fixture`` decorator instead. - See our `docs `__ on information on how to update your code. + See our `docs `__ on information on how to update your code. - `#4545 `_: Calling fixtures directly is now always an error instead of a warning. - See our `docs `__ on information on how to update your code. + See our `docs `__ on information on how to update your code. - `#4546 `_: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check. @@ -2038,12 +2038,12 @@ Removals - `#4547 `_: The deprecated ``record_xml_property`` fixture has been removed, use the more generic ``record_property`` instead. - See our `docs `__ for more information. + See our `docs `__ for more information. - `#4548 `_: An error is now raised if the ``pytest_plugins`` variable is defined in a non-top-level ``conftest.py`` file (i.e., not residing in the ``rootdir``). - See our `docs `__ for more information. + See our `docs `__ for more information. - `#891 `_: Remove ``testfunction.markername`` attributes - use ``Node.iter_markers(name=None)`` to iterate them. @@ -2055,7 +2055,7 @@ Deprecations - `#3050 `_: Deprecated the ``pytest.config`` global. - See https://docs.pytest.org/en/latest/deprecations.html#pytest-config-global for rationale. + See https://docs.pytest.org/en/stable/deprecations.html#pytest-config-global for rationale. - `#3974 `_: Passing the ``message`` parameter of ``pytest.raises`` now issues a ``DeprecationWarning``. @@ -2070,7 +2070,7 @@ Deprecations - `#4435 `_: Deprecated ``raises(..., 'code(as_a_string)')`` and ``warns(..., 'code(as_a_string)')``. - See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec for rationale and examples. + See https://docs.pytest.org/en/stable/deprecations.html#raises-warns-exec for rationale and examples. @@ -2264,7 +2264,7 @@ Removals instead of warning messages. **The affected features will be effectively removed in pytest 4.1**, so please consult the - `Deprecations and Removals `__ + `Deprecations and Removals `__ section in the docs for directions on how to update existing code. In the pytest ``4.0.X`` series, it is possible to change the errors back into warnings as a stop @@ -2364,7 +2364,7 @@ Features existing ``pytest_enter_pdb`` hook. -- `#4147 `_: Add ``--sw``, ``--stepwise`` as an alternative to ``--lf -x`` for stopping at the first failure, but starting the next test invocation from that test. See `the documentation `__ for more info. +- `#4147 `_: Add ``--sw``, ``--stepwise`` as an alternative to ``--lf -x`` for stopping at the first failure, but starting the next test invocation from that test. See `the documentation `__ for more info. - `#4188 `_: Make ``--color`` emit colorful dots when not running in verbose mode. Earlier, it would only colorize the test-by-test output if ``--verbose`` was also passed. @@ -2516,7 +2516,7 @@ Deprecations Users should just ``import pytest`` and access those objects using the ``pytest`` module. * ``request.cached_setup``, this was the precursor of the setup/teardown mechanism available to fixtures. You can - consult `funcarg comparison section in the docs `_. + consult `funcarg comparison section in the docs `_. * Using objects named ``"Class"`` as a way to customize the type of nodes that are collected in ``Collector`` subclasses has been deprecated. Users instead should use ``pytest_collect_make_item`` to customize node types during @@ -2729,7 +2729,7 @@ Bug Fixes Improved Documentation ---------------------- -- `#3996 `_: New `Deprecations and Removals `_ page shows all currently +- `#3996 `_: New `Deprecations and Removals `_ page shows all currently deprecated features, the rationale to do so, and alternatives to update your code. It also list features removed from pytest in past major releases to help those with ancient pytest versions to upgrade. @@ -2751,7 +2751,7 @@ Deprecations and Removals ------------------------- - `#2452 `_: ``Config.warn`` and ``Node.warn`` have been - deprecated, see ``_ for rationale and + deprecated, see ``_ for rationale and examples. - `#3936 `_: ``@pytest.mark.filterwarnings`` second parameter is no longer regex-escaped, @@ -2769,13 +2769,13 @@ Features the standard warnings filters to manage those warnings. This introduces ``PytestWarning``, ``PytestDeprecationWarning`` and ``RemovedInPytest4Warning`` warning types as part of the public API. - Consult `the documentation `__ for more info. + Consult `the documentation `__ for more info. - `#2908 `_: ``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is configured. This makes pytest more compliant with `PEP-0506 `_. See - `the docs `_ for + `the docs `_ for more info. @@ -2969,10 +2969,10 @@ pytest 3.7.0 (2018-07-30) Deprecations and Removals ------------------------- -- `#2639 `_: ``pytest_namespace`` has been `deprecated `_. +- `#2639 `_: ``pytest_namespace`` has been `deprecated `_. -- `#3661 `_: Calling a fixture function directly, as opposed to request them in a test function, now issues a ``RemovedInPytest4Warning``. See `the documentation for rationale and examples `_. +- `#3661 `_: Calling a fixture function directly, as opposed to request them in a test function, now issues a ``RemovedInPytest4Warning``. See `the documentation for rationale and examples `_. @@ -3196,9 +3196,9 @@ Features design. This introduces new ``Node.iter_markers(name)`` and ``Node.get_closest_marker(name)`` APIs. Users are **strongly encouraged** to read the `reasons for the revamp in the docs - `_, + `_, or jump over to details about `updating existing code to use the new APIs - `_. + `_. (`#3317 `_) - Now when ``@pytest.fixture`` is applied more than once to the same function a @@ -3208,7 +3208,7 @@ Features - Support for Python 3.7's builtin ``breakpoint()`` method, see `Using the builtin breakpoint function - `_ for + `_ for details. (`#3180 `_) - ``monkeypatch`` now supports a ``context()`` function which acts as a context @@ -3334,7 +3334,7 @@ Deprecations and Removals `_) - Defining ``pytest_plugins`` is now deprecated in non-top-level conftest.py - files, because they "leak" to the entire directory tree. `See the docs `_ for the rationale behind this decision (`#3084 + files, because they "leak" to the entire directory tree. `See the docs `_ for the rationale behind this decision (`#3084 `_) @@ -3348,7 +3348,7 @@ Features - New ``--rootdir`` command-line option to override the rules for discovering the root directory. See `customize - `_ in the documentation for + `_ in the documentation for details. (`#1642 `_) - Fixtures are now instantiated based on their scopes, with higher-scoped @@ -3435,7 +3435,7 @@ Bug Fixes Improved Documentation ---------------------- -- Added a `reference `_ page +- Added a `reference `_ page to the docs. (`#1713 `_) @@ -3595,9 +3595,9 @@ Features `_) - **Incompatible change**: after community feedback the `logging - `_ functionality has + `_ functionality has undergone some changes. Please consult the `logging documentation - `_ + `_ for details. (`#3013 `_) - Console output falls back to "classic" mode when capturing is disabled (``-s``), @@ -3605,10 +3605,10 @@ Features `_) - New `pytest_runtest_logfinish - `_ + `_ hook which is called when a test item has finished executing, analogous to `pytest_runtest_logstart - `_. + `_. (`#3101 `_) - Improve performance when collecting tests using many fixtures. (`#3107 @@ -3850,7 +3850,7 @@ Features markers. Also, a ``caplog`` fixture is available that enables users to test the captured log during specific tests (similar to ``capsys`` for example). For more information, please see the `logging docs - `_. This feature was + `_. This feature was introduced by merging the popular `pytest-catchlog `_ plugin, thanks to `Thomas Hisch `_. Be advised that during the merging the @@ -4146,7 +4146,7 @@ Deprecations and Removals - ``pytest.approx`` no longer supports ``>``, ``>=``, ``<`` and ``<=`` operators to avoid surprising/inconsistent behavior. See `the approx docs - `_ for more + `_ for more information. (`#2003 `_) - All old-style specific behavior in current classes in the pytest's API is @@ -4192,13 +4192,13 @@ Features - Introduce the ``PYTEST_CURRENT_TEST`` environment variable that is set with the ``nodeid`` and stage (``setup``, ``call`` and ``teardown``) of the test being currently executed. See the `documentation - `_ for more info. (`#2583 `_) - Introduced ``@pytest.mark.filterwarnings`` mark which allows overwriting the warnings filter on a per test, class or module level. See the `docs - `_ for more information. (`#2598 `_) @@ -4428,7 +4428,7 @@ New Features [pytest] addopts = -p no:warnings - See the `warnings documentation page `_ for more + See the `warnings documentation page `_ for more information. Thanks `@nicoddemus`_ for the PR. @@ -5502,7 +5502,7 @@ time or change existing behaviors in order to make them less surprising/more use * Fix (`#1422`_): junit record_xml_property doesn't allow multiple records with same name. -.. _`traceback style docs`: https://pytest.org/en/latest/usage.html#modifying-python-traceback-printing +.. _`traceback style docs`: https://pytest.org/en/stable/usage.html#modifying-python-traceback-printing .. _#1609: https://github.com/pytest-dev/pytest/issues/1609 .. _#1422: https://github.com/pytest-dev/pytest/issues/1422 @@ -6020,7 +6020,7 @@ time or change existing behaviors in order to make them less surprising/more use - add ability to set command line options by environment variable PYTEST_ADDOPTS. - added documentation on the new pytest-dev teams on bitbucket and - github. See https://pytest.org/en/latest/contributing.html . + github. See https://pytest.org/en/stable/contributing.html . Thanks to Anatoly for pushing and initial work on this. - fix issue650: new option ``--docttest-ignore-import-errors`` which @@ -6761,7 +6761,7 @@ Bug fixes: - yielded test functions will now have autouse-fixtures active but cannot accept fixtures as funcargs - it's anyway recommended to rather use the post-2.0 parametrize features instead of yield, see: - http://pytest.org/en/latest/example/parametrize.html + http://pytest.org/en/stable/example/parametrize.html - fix autouse-issue where autouse-fixtures would not be discovered if defined in an a/conftest.py file and tests in a/tests/test_some.py - fix issue226 - LIFO ordering for fixture teardowns @@ -6894,7 +6894,7 @@ Bug fixes: - pluginmanager.register(...) now raises ValueError if the plugin has been already registered or the name is taken -- fix issue159: improve http://pytest.org/en/latest/faq.html +- fix issue159: improve http://pytest.org/en/stable/faq.html especially with respect to the "magic" history, also mention pytest-django, trial and unittest integration. @@ -7007,7 +7007,7 @@ Bug fixes: or through plugin hooks. Also introduce a "--strict" option which will treat unregistered markers as errors allowing to avoid typos and maintain a well described set of markers - for your test suite. See exaples at http://pytest.org/en/latest/mark.html + for your test suite. See exaples at http://pytest.org/en/stable/mark.html and its links. - issue50: introduce "-m marker" option to select tests based on markers (this is a stricter and more predictable version of '-k' in that "-m" @@ -7190,7 +7190,7 @@ Bug fixes: - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output - introduce a mechanism to prevent/unregister plugins from the - command line, see http://pytest.org/en/latest/plugins.html#cmdunregister + command line, see http://pytest.org/en/stable/plugins.html#cmdunregister - activate resultlog plugin by default - fix regression wrt yielded tests which due to the collection-before-running semantics were not diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index dba02248bfd..ccf31cd8bbf 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -404,7 +404,7 @@ This should be updated to make use of standard fixture mechanisms: session.close() -You can consult `funcarg comparison section in the docs `_ for +You can consult `funcarg comparison section in the docs `_ for more information. diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 1fd10101c0e..55dd8af234f 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -224,17 +224,17 @@ You can ask which markers exist for your test suite - the list includes our just $ pytest --markers @pytest.mark.webtest: mark a test as a webtest. - @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/latest/warnings.html#pytest-mark-filterwarnings + @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/stable/warnings.html#pytest-mark-filterwarnings @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test. - @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see https://docs.pytest.org/en/latest/skipping.html + @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see https://docs.pytest.org/en/stable/skipping.html - @pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False): mark the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/latest/skipping.html + @pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False): mark the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/stable/skipping.html - @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/latest/parametrize.html for more info and examples. + @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/stable/parametrize.html for more info and examples. - @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/latest/fixture.html#usefixtures + @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/fixture.html#usefixtures @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. @@ -429,17 +429,17 @@ The ``--markers`` option always gives you a list of available markers: $ pytest --markers @pytest.mark.env(name): mark test to run only on named environment - @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/latest/warnings.html#pytest-mark-filterwarnings + @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/stable/warnings.html#pytest-mark-filterwarnings @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test. - @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see https://docs.pytest.org/en/latest/skipping.html + @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see https://docs.pytest.org/en/stable/skipping.html - @pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False): mark the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/latest/skipping.html + @pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False): mark the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/stable/skipping.html - @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/latest/parametrize.html for more info and examples. + @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/stable/parametrize.html for more info and examples. - @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/latest/fixture.html#usefixtures + @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/fixture.html#usefixtures @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. diff --git a/doc/en/flaky.rst b/doc/en/flaky.rst index c246f4d9c18..b6fc1520c0b 100644 --- a/doc/en/flaky.rst +++ b/doc/en/flaky.rst @@ -28,7 +28,7 @@ Flaky tests sometimes appear when a test suite is run in parallel (such as use o Overly strict assertion ~~~~~~~~~~~~~~~~~~~~~~~ -Overly strict assertions can cause problems with floating point comparison as well as timing issues. `pytest.approx `_ is useful here. +Overly strict assertions can cause problems with floating point comparison as well as timing issues. `pytest.approx `_ is useful here. Pytest features diff --git a/doc/en/funcarg_compare.rst b/doc/en/funcarg_compare.rst index 4350c98b670..33b19ab0f35 100644 --- a/doc/en/funcarg_compare.rst +++ b/doc/en/funcarg_compare.rst @@ -7,7 +7,7 @@ pytest-2.3: reasoning for fixture/funcarg evolution **Target audience**: Reading this document requires basic knowledge of python testing, xUnit setup methods and the (previous) basic pytest -funcarg mechanism, see https://docs.pytest.org/en/latest/historical-notes.html#funcargs-and-pytest-funcarg. +funcarg mechanism, see https://docs.pytest.org/en/stable/historical-notes.html#funcargs-and-pytest-funcarg. If you are new to pytest, then you can simply ignore this section and read the other sections. diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 61a0baf1985..58bab610a92 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -207,7 +207,7 @@ This is outlined below: Request a unique temporary directory for functional tests -------------------------------------------------------------- -``pytest`` provides `Builtin fixtures/function arguments `_ to request arbitrary resources, like a unique temporary directory: +``pytest`` provides `Builtin fixtures/function arguments `_ to request arbitrary resources, like a unique temporary directory: .. code-block:: python diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 02457d015a7..e25793f8769 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -698,7 +698,7 @@ by the `PyPy-test`_ web page to show test results over several revisions. If you use this option, consider using the new `pytest-reportlog `__ plugin instead. - See `the deprecation docs `__ + See `the deprecation docs `__ for more information. diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 42c74ef368d..0b47009d680 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -40,7 +40,7 @@ Running pytest now produces this output: $REGENDOC_TMPDIR/test_show_warnings.py:5: UserWarning: api v1, should use functions from v2 warnings.warn(UserWarning("api v1, should use functions from v2")) - -- Docs: https://docs.pytest.org/en/latest/warnings.html + -- Docs: https://docs.pytest.org/en/stable/warnings.html ======================= 1 passed, 1 warning in 0.12s ======================= The ``-W`` flag can be passed to control which warnings will be displayed or even turn @@ -407,7 +407,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta $REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestCollectionWarning: cannot collect test class 'Test' because it has a __init__ constructor (from: test_pytest_warnings.py) class Test: - -- Docs: https://docs.pytest.org/en/latest/warnings.html + -- Docs: https://docs.pytest.org/en/stable/warnings.html 1 warning in 0.12s These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings. diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 5ee634d575f..27d0fb57c5e 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -444,10 +444,10 @@ additionally it is possible to copy examples for an example folder before runnin test_example.py::test_plugin $PYTHON_PREFIX/lib/python3.8/site-packages/_pytest/compat.py:333: PytestDeprecationWarning: The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk. - See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information. + See https://docs.pytest.org/en/stable/deprecations.html#terminalreporter-writer for more information. return getattr(object, name, default) - -- Docs: https://docs.pytest.org/en/latest/warnings.html + -- Docs: https://docs.pytest.org/en/stable/warnings.html ====================== 2 passed, 2 warnings in 0.12s ======================= For more information about the result object that ``runpytest()`` returns, and diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 967272ca6ba..00f62b60c77 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -42,7 +42,7 @@ **Do not** commit this to version control. -See [the docs](https://docs.pytest.org/en/latest/cache.html) for more information. +See [the docs](https://docs.pytest.org/en/stable/cache.html) for more information. """ CACHEDIR_TAG_CONTENT = b"""\ diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index b5cff73016a..f4e0d5d0fab 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -589,7 +589,7 @@ def _check_non_top_pytest_plugins( "Please move it to a top level conftest file at the rootdir:\n" " {}\n" "For more information, visit:\n" - " https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" + " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" ) fail(msg.format(conftestpath, self._confcutdir), pytrace=False) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 1ce4e1e39b2..868318a2bc6 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -32,7 +32,7 @@ RESULT_LOG = PytestDeprecationWarning( "--result-log is deprecated, please try the new pytest-reportlog plugin.\n" - "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." + "See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information." ) FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning( @@ -44,13 +44,13 @@ PytestDeprecationWarning, "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n" "See " - "https://docs.pytest.org/en/latest/deprecations.html#node-construction-changed-to-node-from-parent" + "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent" " for more details.", ) JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning( "The 'junit_family' default value will change to 'xunit2' in pytest 6.0. See:\n" - " https://docs.pytest.org/en/latest/deprecations.html#junit-family-default-value-change-to-xunit2\n" + " https://docs.pytest.org/en/stable/deprecations.html#junit-family-default-value-change-to-xunit2\n" "for more information." ) @@ -68,7 +68,7 @@ TERMINALWRITER_WRITER = PytestDeprecationWarning( "The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.\n" - "See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information." + "See https://docs.pytest.org/en/stable/deprecations.html#terminalreporter-writer for more information." ) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 9423df7e4c3..cedcd462559 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1148,8 +1148,8 @@ def wrap_function_to_error_out_if_called_directly(function, fixture_marker): message = ( 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' "but are created automatically when test functions request them as parameters.\n" - "See https://docs.pytest.org/en/latest/fixture.html for more information about fixtures, and\n" - "https://docs.pytest.org/en/latest/deprecations.html#calling-fixtures-directly about how to update your code." + "See https://docs.pytest.org/en/stable/fixture.html for more information about fixtures, and\n" + "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code." ).format(name=fixture_marker.name or function.__name__) @functools.wraps(function) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index ca5c03e901e..0c9344f3fc0 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -507,7 +507,7 @@ def __getattr__(self, name: str) -> MarkDecorator: warnings.warn( "Unknown pytest.mark.%s - is this a typo? You can register " "custom marks to avoid this warning - for details, see " - "https://docs.pytest.org/en/latest/mark.html" % name, + "https://docs.pytest.org/en/stable/mark.html" % name, PytestUnknownMarkWarning, 2, ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 751e174071a..b41a234f4b8 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -143,14 +143,14 @@ def pytest_configure(config: Config) -> None: "or a list of tuples of values if argnames specifies multiple names. " "Example: @parametrize('arg1', [1,2]) would lead to two calls of the " "decorated test function, one with arg1=1 and another with arg1=2." - "see https://docs.pytest.org/en/latest/parametrize.html for more info " + "see https://docs.pytest.org/en/stable/parametrize.html for more info " "and examples.", ) config.addinivalue_line( "markers", "usefixtures(fixturename1, fixturename2, ...): mark tests as needing " "all of the specified fixtures. see " - "https://docs.pytest.org/en/latest/fixture.html#usefixtures ", + "https://docs.pytest.org/en/stable/fixture.html#usefixtures ", ) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index b312b356256..6c19e56dda8 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -958,7 +958,7 @@ def collapsed_location_report(reports: List[WarningReport]) -> str: message = message.rstrip() self._tw.line(message) self._tw.line() - self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html") + self._tw.line("-- Docs: https://docs.pytest.org/en/stable/warnings.html") def summary_passes(self) -> None: if self.config.option.tbstyle != "no": diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index ee437cc9746..494b92efff6 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -78,7 +78,7 @@ class PytestUnhandledCoroutineWarning(PytestWarning): class PytestUnknownMarkWarning(PytestWarning): """Warning emitted on use of unknown markers. - See https://docs.pytest.org/en/latest/mark.html for details. + See https://docs.pytest.org/en/stable/mark.html for details. """ __module__ = "pytest" diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index f92350b20d7..33b01b79707 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -76,7 +76,7 @@ def pytest_configure(config: Config) -> None: config.addinivalue_line( "markers", "filterwarnings(warning): add a warning filter to the given test. " - "see https://docs.pytest.org/en/latest/warnings.html#pytest-mark-filterwarnings ", + "see https://docs.pytest.org/en/stable/warnings.html#pytest-mark-filterwarnings ", ) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index b5ad94861ae..7cce092df6c 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -23,7 +23,7 @@ def test(): result.stdout.fnmatch_lines( [ "*--result-log is deprecated, please try the new pytest-reportlog plugin.", - "*See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information*", + "*See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information*", ] ) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index fc327e6c058..448900501b1 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -363,7 +363,7 @@ def test_plugin_prevent_register_stepwise_on_cacheprovider_unregister( self, pytestpm ): """ From PR #4304 : The only way to unregister a module is documented at - the end of https://docs.pytest.org/en/latest/plugins.html. + the end of https://docs.pytest.org/en/stable/plugins.html. When unregister cacheprovider, then unregister stepwise too """ From 678c1a0745f1cf175c442c719906a1f13e496910 Mon Sep 17 00:00:00 2001 From: Vlad-Radz <57449367+Vlad-Radz@users.noreply.github.com> Date: Wed, 8 Jul 2020 18:04:56 +0200 Subject: [PATCH 448/823] assertion: improve diff output of recursive dataclass/attrs Co-authored-by: Vlad --- AUTHORS | 1 + changelog/7348.improvement.rst | 1 + src/_pytest/assertion/util.py | 15 ++- .../test_compare_recursive_dataclasses.py | 38 ++++---- testing/test_assertion.py | 91 +++++++++++-------- testing/test_error_diffs.py | 12 ++- 6 files changed, 100 insertions(+), 58 deletions(-) create mode 100644 changelog/7348.improvement.rst diff --git a/AUTHORS b/AUTHORS index e4023768204..4cdf231b146 100644 --- a/AUTHORS +++ b/AUTHORS @@ -293,6 +293,7 @@ Vidar T. Fauske Virgil Dupras Vitaly Lashmanov Vlad Dragos +Vlad Radziuk Vladyslav Rachek Volodymyr Piskun Wei Lin diff --git a/changelog/7348.improvement.rst b/changelog/7348.improvement.rst new file mode 100644 index 00000000000..714892e6e4e --- /dev/null +++ b/changelog/7348.improvement.rst @@ -0,0 +1 @@ +Improve recursive diff report for comparison asserts on dataclasses / attrs. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index c2f0431d479..554aec77fa9 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -169,7 +169,7 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: - explanation = [] # type: List[str] + explanation = [] if istext(left) and istext(right): explanation = _diff_text(left, right, verbose) else: @@ -424,6 +424,7 @@ def _compare_eq_cls( field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD) ] + indent = " " same = [] diff = [] for field in fields_to_check: @@ -433,6 +434,8 @@ def _compare_eq_cls( diff.append(field) explanation = [] + if same or diff: + explanation += [""] if same and verbose < 2: explanation.append("Omitting %s identical items, use -vv to show" % len(same)) elif same: @@ -440,12 +443,18 @@ def _compare_eq_cls( explanation += pprint.pformat(same).splitlines() if diff: explanation += ["Differing attributes:"] + explanation += pprint.pformat(diff).splitlines() for field in diff: + field_left = getattr(left, field) + field_right = getattr(right, field) explanation += [ - ("%s: %r != %r") % (field, getattr(left, field), getattr(right, field)), "", "Drill down into differing attribute %s:" % field, - *_compare_eq_any(getattr(left, field), getattr(right, field), verbose), + ("%s%s: %r != %r") % (indent, field, field_left, field_right), + ] + explanation += [ + indent + line + for line in _compare_eq_any(field_left, field_right, verbose) ] return explanation diff --git a/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py index 516e36e5c85..167140e16a6 100644 --- a/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py +++ b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py @@ -1,34 +1,38 @@ from dataclasses import dataclass -from dataclasses import field @dataclass -class SimpleDataObject: - field_a: int = field() - field_b: str = field() +class S: + a: int + b: str @dataclass -class ComplexDataObject2: - field_a: SimpleDataObject = field() - field_b: SimpleDataObject = field() +class C: + c: S + d: S @dataclass -class ComplexDataObject: - field_a: SimpleDataObject = field() - field_b: ComplexDataObject2 = field() +class C2: + e: C + f: S -def test_recursive_dataclasses(): +@dataclass +class C3: + g: S + h: C2 + i: str + j: str - left = ComplexDataObject( - SimpleDataObject(1, "b"), - ComplexDataObject2(SimpleDataObject(1, "b"), SimpleDataObject(2, "c"),), + +def test_recursive_dataclasses(): + left = C3( + S(10, "ten"), C2(C(S(1, "one"), S(2, "two")), S(2, "three")), "equal", "left", ) - right = ComplexDataObject( - SimpleDataObject(1, "b"), - ComplexDataObject2(SimpleDataObject(1, "b"), SimpleDataObject(3, "c"),), + right = C3( + S(20, "xxx"), C2(C(S(1, "one"), S(2, "yyy")), S(3, "three")), "equal", "right", ) assert left == right diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 5c9bb35fadc..6723a707e19 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -778,12 +778,16 @@ def test_dataclasses(self, testdir): result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ - "*Omitting 1 identical items, use -vv to show*", - "*Differing attributes:*", - "*field_b: 'b' != 'c'*", - "*- c*", - "*+ b*", - ] + "E Omitting 1 identical items, use -vv to show", + "E Differing attributes:", + "E ['field_b']", + "E ", + "E Drill down into differing attribute field_b:", + "E field_b: 'b' != 'c'...", + "E ", + "E ...Full output truncated (3 lines hidden), use '-vv' to show", + ], + consecutive=True, ) @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") @@ -793,14 +797,16 @@ def test_recursive_dataclasses(self, testdir): result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ - "*Omitting 1 identical items, use -vv to show*", - "*Differing attributes:*", - "*field_b: ComplexDataObject2(*SimpleDataObject(field_a=2, field_b='c')) != ComplexDataObject2(*SimpleDataObject(field_a=3, field_b='c'))*", # noqa - "*Drill down into differing attribute field_b:*", - "*Omitting 1 identical items, use -vv to show*", - "*Differing attributes:*", - "*Full output truncated*", - ] + "E Omitting 1 identical items, use -vv to show", + "E Differing attributes:", + "E ['g', 'h', 'j']", + "E ", + "E Drill down into differing attribute g:", + "E g: S(a=10, b='ten') != S(a=20, b='xxx')...", + "E ", + "E ...Full output truncated (52 lines hidden), use '-vv' to show", + ], + consecutive=True, ) @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") @@ -810,19 +816,30 @@ def test_recursive_dataclasses_verbose(self, testdir): result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ - "*Matching attributes:*", - "*['field_a']*", - "*Differing attributes:*", - "*field_b: ComplexDataObject2(*SimpleDataObject(field_a=2, field_b='c')) != ComplexDataObject2(*SimpleDataObject(field_a=3, field_b='c'))*", # noqa - "*Matching attributes:*", - "*['field_a']*", - "*Differing attributes:*", - "*field_b: SimpleDataObject(field_a=2, field_b='c') != SimpleDataObject(field_a=3, field_b='c')*", # noqa - "*Matching attributes:*", - "*['field_b']*", - "*Differing attributes:*", - "*field_a: 2 != 3", - ] + "E Matching attributes:", + "E ['i']", + "E Differing attributes:", + "E ['g', 'h', 'j']", + "E ", + "E Drill down into differing attribute g:", + "E g: S(a=10, b='ten') != S(a=20, b='xxx')", + "E ", + "E Differing attributes:", + "E ['a', 'b']", + "E ", + "E Drill down into differing attribute a:", + "E a: 10 != 20", + "E +10", + "E -20", + "E ", + "E Drill down into differing attribute b:", + "E b: 'ten' != 'xxx'", + "E - xxx", + "E + ten", + "E ", + "E Drill down into differing attribute h:", + ], + consecutive=True, ) @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") @@ -868,9 +885,9 @@ class SimpleDataObject: lines = callequal(left, right) assert lines is not None - assert lines[1].startswith("Omitting 1 identical item") + assert lines[2].startswith("Omitting 1 identical item") assert "Matching attributes" not in lines - for line in lines[1:]: + for line in lines[2:]: assert "field_a" not in line def test_attrs_recursive(self) -> None: @@ -910,7 +927,8 @@ class SimpleDataObject: lines = callequal(left, right) assert lines is not None - assert "field_d: 'a' != 'b'" in lines + # indentation in output because of nested object structure + assert " field_d: 'a' != 'b'" in lines def test_attrs_verbose(self) -> None: @attr.s @@ -923,9 +941,9 @@ class SimpleDataObject: lines = callequal(left, right, verbose=2) assert lines is not None - assert lines[1].startswith("Matching attributes:") - assert "Omitting" not in lines[1] - assert lines[2] == "['field_a']" + assert lines[2].startswith("Matching attributes:") + assert "Omitting" not in lines[2] + assert lines[3] == "['field_a']" def test_attrs_with_attribute_comparison_off(self): @attr.s @@ -937,11 +955,12 @@ class SimpleDataObject: right = SimpleDataObject(1, "b") lines = callequal(left, right, verbose=2) + print(lines) assert lines is not None - assert lines[1].startswith("Matching attributes:") + assert lines[2].startswith("Matching attributes:") assert "Omitting" not in lines[1] - assert lines[2] == "['field_a']" - for line in lines[2:]: + assert lines[3] == "['field_a']" + for line in lines[3:]: assert "field_b" not in line def test_comparing_two_different_attrs_classes(self): diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index 473c62a7571..2857df83236 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -233,7 +233,11 @@ def test_this(): E Matching attributes: E ['b'] E Differing attributes: - E a: 1 != 2 + E ['a'] + E Drill down into differing attribute a: + E a: 1 != 2 + E +1 + E -2 """, id="Compare data classes", ), @@ -257,7 +261,11 @@ def test_this(): E Matching attributes: E ['a'] E Differing attributes: - E b: 'spam' != 'eggs' + E ['b'] + E Drill down into differing attribute b: + E b: 'spam' != 'eggs' + E - eggs + E + spam """, id="Compare attrs classes", ), From 9db9f04432120254fc8b5c3d8f22cf52eb90561b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 Jul 2020 18:41:31 -0300 Subject: [PATCH 449/823] Use builtin compile in doc example Follow up to #7438 --- doc/en/example/assertion/failure_demo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index 4bf827904d8..a172a007e8c 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -1,4 +1,3 @@ -import _pytest._code import pytest from pytest import raises @@ -197,7 +196,7 @@ def test_dynamic_compile_shows_nicely(): name = "abc-123" spec = importlib.util.spec_from_loader(name, loader=None) module = importlib.util.module_from_spec(spec) - code = _pytest._code.compile(src, name, "exec") + code = compile(src, name, "exec") exec(code, module.__dict__) sys.modules[name] = module module.foo() From d81269056953466caf6805b5bc19ff014c8dfeea Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 Jul 2020 18:48:02 -0300 Subject: [PATCH 450/823] Adjust regendoc for getting-started --- doc/en/getting-started.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 58bab610a92..bcfd49ae293 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -115,6 +115,8 @@ Execute the test function with “quiet” reporting mode: Group multiple tests in a class -------------------------------------------------------------- +.. regendoc:wipe + Once you develop multiple tests, you may want to group them into a class. pytest makes it easy to create a class containing more than one test: .. code-block:: python @@ -163,8 +165,11 @@ Something to be aware of when grouping tests inside classes is that each test ha Having each test share the same class instance would be very detrimental to test isolation and would promote poor test practices. This is outlined below: +.. regendoc:wipe + .. code-block:: python + # content of test_class_demo.py class TestClassDemoInstance: def test_one(self): assert 0 From 64b19595a5820ba7fc0afbeba5d891361c00c9db Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 8 Jul 2020 17:38:29 -0400 Subject: [PATCH 451/823] Set correct version during regen --- doc/en/Makefile | 1 - scripts/release.py | 12 ++++++++---- tox.ini | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/doc/en/Makefile b/doc/en/Makefile index c8d2c856447..1cffbd463d8 100644 --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -27,7 +27,6 @@ REGENDOC_ARGS := \ --normalize "/in \d.\d\ds/in 0.12s/" \ --normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \ --normalize "@pytest-(\d+)\\.[^ ,]+@pytest-\1.x.y@" \ - --normalize "@(This is pytest version )(\d+)\\.[^ ,]+@\1\2.x.y@" \ --normalize "@py-(\d+)\\.[^ ,]+@py-\1.x.y@" \ --normalize "@pluggy-(\d+)\\.[.\d,]+@pluggy-\1.x.y@" \ --normalize "@hypothesis-(\d+)\\.[.\d,]+@hypothesis-\1.x.y@" \ diff --git a/scripts/release.py b/scripts/release.py index 466051d7e43..443b868f3d7 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -2,6 +2,7 @@ Invoke development tasks. """ import argparse +import os from pathlib import Path from subprocess import call from subprocess import check_call @@ -65,10 +66,13 @@ def announce(version): check_call(["git", "add", str(target)]) -def regen(): +def regen(version): """Call regendoc tool to update examples and pytest output in the docs.""" print(f"{Fore.CYAN}[generate.regen] {Fore.RESET}Updating docs") - check_call(["tox", "-e", "regen"]) + check_call( + ["tox", "-e", "regen"], + env={**os.environ, "SETUPTOOLS_SCM_PRETEND_VERSION": version}, + ) def fix_formatting(): @@ -88,13 +92,13 @@ def check_links(): def pre_release(version, *, skip_check_links): """Generates new docs, release announcements and creates a local tag.""" announce(version) - regen() + regen(version) changelog(version, write_out=True) fix_formatting() if not skip_check_links: check_links() - msg = "Preparing release version {}".format(version) + msg = "Prepare release version {}".format(version) check_call(["git", "commit", "-a", "-m", msg]) print() diff --git a/tox.ini b/tox.ini index affb4a7a92c..c8165a3c3bb 100644 --- a/tox.ini +++ b/tox.ini @@ -97,8 +97,8 @@ commands = [testenv:regen] changedir = doc/en -skipsdist = True basepython = python3 +passenv = SETUPTOOLS_SCM_PRETEND_VERSION deps = dataclasses PyYAML From 7d033a89505484d03b1fc4a885ecea1777ca7457 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 8 Jul 2020 17:51:01 -0400 Subject: [PATCH 452/823] Prepare release version 6.0.0rc1 --- changelog/1120.bugfix.rst | 1 - changelog/1316.breaking.rst | 1 - changelog/1556.feature.rst | 17 -- changelog/3342.feature.rst | 10 - changelog/4049.feature.rst | 3 - changelog/4375.improvement.rst | 3 - changelog/4391.improvement.rst | 1 - changelog/4583.bugfix.rst | 1 - changelog/4675.improvement.rst | 1 - changelog/4677.bugfix.rst | 1 - changelog/5456.bugfix.rst | 2 - changelog/5965.breaking.rst | 9 - changelog/6240.bugfix.rst | 2 - changelog/6285.feature.rst | 2 - changelog/6428.bugfix.rst | 2 - changelog/6433.feature.rst | 10 - changelog/6471.feature.rst | 4 - changelog/6505.breaking.rst | 20 -- changelog/6755.bugfix.rst | 1 - changelog/6817.improvement.rst | 2 - changelog/6856.feature.rst | 3 - changelog/6871.bugfix.rst | 1 - changelog/6903.breaking.rst | 2 - changelog/6906.feature.rst | 1 - changelog/6909.bugfix.rst | 3 - changelog/6910.bugfix.rst | 1 - changelog/6924.bugfix.rst | 1 - changelog/6925.bugfix.rst | 1 - changelog/6940.improvement.rst | 1 - changelog/6947.bugfix.rst | 1 - changelog/6951.bugfix.rst | 1 - changelog/6956.bugfix.rst | 1 - changelog/6981.deprecation.rst | 1 - changelog/6991.bugfix.rst | 1 - changelog/6991.improvement.rst | 1 - changelog/6992.bugfix.rst | 1 - changelog/7035.trivial.rst | 2 - changelog/7040.breaking.rst | 6 - changelog/7061.bugfix.rst | 1 - changelog/7076.bugfix.rst | 1 - changelog/7091.improvement.rst | 4 - changelog/7097.deprecation.rst | 6 - changelog/7110.bugfix.rst | 1 - changelog/7119.improvement.rst | 2 - changelog/7122.breaking.rst | 3 - changelog/7126.bugfix.rst | 2 - changelog/7128.improvement.rst | 1 - changelog/7133.improvement.rst | 1 - changelog/7135.breaking.rst | 15 - changelog/7143.bugfix.rst | 1 - changelog/7145.bugfix.rst | 1 - changelog/7150.bugfix.rst | 1 - changelog/7159.improvement.rst | 3 - changelog/7180.bugfix.rst | 1 - changelog/7202.doc.rst | 1 - changelog/7210.deprecation.rst | 5 - changelog/7215.bugfix.rst | 2 - changelog/7224.breaking.rst | 4 - changelog/7226.breaking.rst | 1 - changelog/7233.doc.rst | 1 - changelog/7245.feature.rst | 14 - changelog/7253.bugfix.rst | 3 - changelog/7264.improvement.rst | 1 - changelog/7291.trivial.rst | 1 - changelog/7295.trivial.rst | 1 - changelog/7305.feature.rst | 1 - changelog/7345.doc.rst | 1 - changelog/7346.feature.rst | 1 - changelog/7348.improvement.rst | 1 - changelog/7356.trivial.rst | 1 - changelog/7357.trivial.rst | 1 - changelog/7360.bugfix.rst | 2 - changelog/7383.bugfix.rst | 1 - changelog/7385.improvement.rst | 13 - changelog/7418.breaking.rst | 2 - changelog/7438.breaking.rst | 11 - doc/en/announce/index.rst | 1 + doc/en/announce/release-6.0.0rc1.rst | 69 +++++ doc/en/assert.rst | 4 +- doc/en/cache.rst | 36 +-- doc/en/capture.rst | 2 +- doc/en/changelog.rst | 417 +++++++++++++++++++++++++++ doc/en/doctest.rst | 4 +- doc/en/example/markers.rst | 38 +-- doc/en/example/nonpython.rst | 9 +- doc/en/example/parametrize.rst | 33 ++- doc/en/example/pythoncollection.rst | 14 +- doc/en/example/reportingdemo.rst | 123 ++++---- doc/en/example/simple.rst | 24 +- doc/en/fixture.rst | 13 +- doc/en/getting-started.rst | 35 +-- doc/en/index.rst | 2 +- doc/en/parametrize.rst | 4 +- doc/en/skipping.rst | 2 +- doc/en/tmpdir.rst | 4 +- doc/en/unittest.rst | 2 +- doc/en/usage.rst | 10 +- doc/en/warnings.rst | 2 +- doc/en/writing_plugins.rst | 6 +- 99 files changed, 678 insertions(+), 411 deletions(-) delete mode 100644 changelog/1120.bugfix.rst delete mode 100644 changelog/1316.breaking.rst delete mode 100644 changelog/1556.feature.rst delete mode 100644 changelog/3342.feature.rst delete mode 100644 changelog/4049.feature.rst delete mode 100644 changelog/4375.improvement.rst delete mode 100644 changelog/4391.improvement.rst delete mode 100644 changelog/4583.bugfix.rst delete mode 100644 changelog/4675.improvement.rst delete mode 100644 changelog/4677.bugfix.rst delete mode 100644 changelog/5456.bugfix.rst delete mode 100644 changelog/5965.breaking.rst delete mode 100644 changelog/6240.bugfix.rst delete mode 100644 changelog/6285.feature.rst delete mode 100644 changelog/6428.bugfix.rst delete mode 100644 changelog/6433.feature.rst delete mode 100644 changelog/6471.feature.rst delete mode 100644 changelog/6505.breaking.rst delete mode 100644 changelog/6755.bugfix.rst delete mode 100644 changelog/6817.improvement.rst delete mode 100644 changelog/6856.feature.rst delete mode 100644 changelog/6871.bugfix.rst delete mode 100644 changelog/6903.breaking.rst delete mode 100644 changelog/6906.feature.rst delete mode 100644 changelog/6909.bugfix.rst delete mode 100644 changelog/6910.bugfix.rst delete mode 100644 changelog/6924.bugfix.rst delete mode 100644 changelog/6925.bugfix.rst delete mode 100644 changelog/6940.improvement.rst delete mode 100644 changelog/6947.bugfix.rst delete mode 100644 changelog/6951.bugfix.rst delete mode 100644 changelog/6956.bugfix.rst delete mode 100644 changelog/6981.deprecation.rst delete mode 100644 changelog/6991.bugfix.rst delete mode 100644 changelog/6991.improvement.rst delete mode 100644 changelog/6992.bugfix.rst delete mode 100644 changelog/7035.trivial.rst delete mode 100644 changelog/7040.breaking.rst delete mode 100644 changelog/7061.bugfix.rst delete mode 100644 changelog/7076.bugfix.rst delete mode 100644 changelog/7091.improvement.rst delete mode 100644 changelog/7097.deprecation.rst delete mode 100644 changelog/7110.bugfix.rst delete mode 100644 changelog/7119.improvement.rst delete mode 100644 changelog/7122.breaking.rst delete mode 100644 changelog/7126.bugfix.rst delete mode 100644 changelog/7128.improvement.rst delete mode 100644 changelog/7133.improvement.rst delete mode 100644 changelog/7135.breaking.rst delete mode 100644 changelog/7143.bugfix.rst delete mode 100644 changelog/7145.bugfix.rst delete mode 100644 changelog/7150.bugfix.rst delete mode 100644 changelog/7159.improvement.rst delete mode 100644 changelog/7180.bugfix.rst delete mode 100644 changelog/7202.doc.rst delete mode 100644 changelog/7210.deprecation.rst delete mode 100644 changelog/7215.bugfix.rst delete mode 100644 changelog/7224.breaking.rst delete mode 100644 changelog/7226.breaking.rst delete mode 100644 changelog/7233.doc.rst delete mode 100644 changelog/7245.feature.rst delete mode 100644 changelog/7253.bugfix.rst delete mode 100644 changelog/7264.improvement.rst delete mode 100644 changelog/7291.trivial.rst delete mode 100644 changelog/7295.trivial.rst delete mode 100644 changelog/7305.feature.rst delete mode 100644 changelog/7345.doc.rst delete mode 100644 changelog/7346.feature.rst delete mode 100644 changelog/7348.improvement.rst delete mode 100644 changelog/7356.trivial.rst delete mode 100644 changelog/7357.trivial.rst delete mode 100644 changelog/7360.bugfix.rst delete mode 100644 changelog/7383.bugfix.rst delete mode 100644 changelog/7385.improvement.rst delete mode 100644 changelog/7418.breaking.rst delete mode 100644 changelog/7438.breaking.rst create mode 100644 doc/en/announce/release-6.0.0rc1.rst diff --git a/changelog/1120.bugfix.rst b/changelog/1120.bugfix.rst deleted file mode 100644 index 95e74fa75b0..00000000000 --- a/changelog/1120.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix issue where directories from tmpdir are not removed properly when multiple instances of pytest are running in parallel. diff --git a/changelog/1316.breaking.rst b/changelog/1316.breaking.rst deleted file mode 100644 index 4c01de728e8..00000000000 --- a/changelog/1316.breaking.rst +++ /dev/null @@ -1 +0,0 @@ -``TestReport.longrepr`` is now always an instance of ``ReprExceptionInfo``. Previously it was a ``str`` when a test failed with ``pytest.fail(..., pytrace=False)``. diff --git a/changelog/1556.feature.rst b/changelog/1556.feature.rst deleted file mode 100644 index 402e772e674..00000000000 --- a/changelog/1556.feature.rst +++ /dev/null @@ -1,17 +0,0 @@ -pytest now supports ``pyproject.toml`` files for configuration. - -The configuration options is similar to the one available in other formats, but must be defined -in a ``[tool.pytest.ini_options]`` table to be picked up by pytest: - -.. code-block:: toml - - # pyproject.toml - [tool.pytest.ini_options] - minversion = "6.0" - addopts = "-ra -q" - testpaths = [ - "tests", - "integration", - ] - -More information can be found `in the docs `__. diff --git a/changelog/3342.feature.rst b/changelog/3342.feature.rst deleted file mode 100644 index aef7e2b0466..00000000000 --- a/changelog/3342.feature.rst +++ /dev/null @@ -1,10 +0,0 @@ -pytest now includes inline type annotations and exposes them to user programs. -Most of the user-facing API is covered, as well as internal code. - -If you are running a type checker such as mypy on your tests, you may start -noticing type errors indicating incorrect usage. If you run into an error that -you believe to be incorrect, please let us know in an issue. - -The types were developed against mypy version 0.780. Older versions may work, -but we recommend using at least this version. Other type checkers may work as -well, but they are not officially verified to work by pytest yet. diff --git a/changelog/4049.feature.rst b/changelog/4049.feature.rst deleted file mode 100644 index 4073589b05a..00000000000 --- a/changelog/4049.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Introduced a new hook named `pytest_warning_recorded` to convey information about warnings captured by the internal `pytest` warnings plugin. - -This hook is meant to replace `pytest_warning_captured`, which will be removed in a future release. diff --git a/changelog/4375.improvement.rst b/changelog/4375.improvement.rst deleted file mode 100644 index 0c9a7f3e67a..00000000000 --- a/changelog/4375.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -The ``pytest`` command now supresses the ``BrokenPipeError`` error message that -is printed to stderr when the output of ``pytest`` is piped and and the pipe is -closed by the piped-to program (common examples are ``less`` and ``head``). diff --git a/changelog/4391.improvement.rst b/changelog/4391.improvement.rst deleted file mode 100644 index e7e4090f1dd..00000000000 --- a/changelog/4391.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Improved precision of test durations measurement. ``CallInfo`` items now have a new ``.duration`` attribute, created using ``time.perf_counter()``. This attribute is used to fill the ``.duration`` attribute, which is more accurate than the previous ``.stop - .start`` (as these are based on ``time.time()``). diff --git a/changelog/4583.bugfix.rst b/changelog/4583.bugfix.rst deleted file mode 100644 index f0a82030338..00000000000 --- a/changelog/4583.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Prevent crashing and provide a user-friendly error when a marker expression (-m) invoking of eval() raises any exception. diff --git a/changelog/4675.improvement.rst b/changelog/4675.improvement.rst deleted file mode 100644 index c90cd3591ee..00000000000 --- a/changelog/4675.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Rich comparison for dataclasses and `attrs`-classes is now recursive. diff --git a/changelog/4677.bugfix.rst b/changelog/4677.bugfix.rst deleted file mode 100644 index 6b7d2cf179d..00000000000 --- a/changelog/4677.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -The path shown in the summary report for SKIPPED tests is now always relative. Previously it was sometimes absolute. diff --git a/changelog/5456.bugfix.rst b/changelog/5456.bugfix.rst deleted file mode 100644 index 17680757052..00000000000 --- a/changelog/5456.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix a possible race condition when trying to remove lock files used to control access to folders -created by ``tmp_path`` and ``tmpdir``. diff --git a/changelog/5965.breaking.rst b/changelog/5965.breaking.rst deleted file mode 100644 index 3ecb9486aed..00000000000 --- a/changelog/5965.breaking.rst +++ /dev/null @@ -1,9 +0,0 @@ -symlinks are no longer resolved during collection and matching `conftest.py` files with test file paths. - -Resolving symlinks for the current directory and during collection was introduced as a bugfix in 3.9.0, but it actually is a new feature which had unfortunate consequences in Windows and surprising results in other platforms. - -The team decided to step back on resolving symlinks at all, planning to review this in the future with a more solid solution (see discussion in -`#6523 `__ for details). - -This might break test suites which made use of this feature; the fix is to create a symlink -for the entire test tree, and not only to partial files/tress as it was possible previously. diff --git a/changelog/6240.bugfix.rst b/changelog/6240.bugfix.rst deleted file mode 100644 index b5f5844ec4a..00000000000 --- a/changelog/6240.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixes an issue where logging during collection step caused duplication of log -messages to stderr. diff --git a/changelog/6285.feature.rst b/changelog/6285.feature.rst deleted file mode 100644 index bac353c86a5..00000000000 --- a/changelog/6285.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Exposed the `pytest.FixtureLookupError` exception which is raised by `request.getfixturevalue()` -(where `request` is a `FixtureRequest` fixture) when a fixture with the given name cannot be returned. diff --git a/changelog/6428.bugfix.rst b/changelog/6428.bugfix.rst deleted file mode 100644 index 581b2b7cece..00000000000 --- a/changelog/6428.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Paths appearing in error messages are now correct in case the current working directory has -changed since the start of the session. diff --git a/changelog/6433.feature.rst b/changelog/6433.feature.rst deleted file mode 100644 index c331b0f5888..00000000000 --- a/changelog/6433.feature.rst +++ /dev/null @@ -1,10 +0,0 @@ -If an error is encountered while formatting the message in a logging call, for -example ``logging.warning("oh no!: %s: %s", "first")`` (a second argument is -missing), pytest now propagates the error, likely causing the test to fail. - -Previously, such a mistake would cause an error to be printed to stderr, which -is not displayed by default for passing tests. This change makes the mistake -visible during testing. - -You may supress this behavior temporarily or permanently by setting -``logging.raiseExceptions = False``. diff --git a/changelog/6471.feature.rst b/changelog/6471.feature.rst deleted file mode 100644 index 28457b9f537..00000000000 --- a/changelog/6471.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -New command-line flags: - -* `--no-header`: disables the initial header, including platform, version, and plugins. -* `--no-summary`: disables the final test summary, including warnings. diff --git a/changelog/6505.breaking.rst b/changelog/6505.breaking.rst deleted file mode 100644 index 164b69485a0..00000000000 --- a/changelog/6505.breaking.rst +++ /dev/null @@ -1,20 +0,0 @@ -``Testdir.run().parseoutcomes()`` now always returns the parsed nouns in plural form. - -Originally ``parseoutcomes()`` would always returns the nouns in plural form, but a change -meant to improve the terminal summary by using singular form single items (``1 warning`` or ``1 error``) -caused an unintended regression by changing the keys returned by ``parseoutcomes()``. - -Now the API guarantees to always return the plural form, so calls like this: - -.. code-block:: python - - result = testdir.runpytest() - result.assert_outcomes(error=1) - -Need to be changed to: - - -.. code-block:: python - - result = testdir.runpytest() - result.assert_outcomes(errors=1) diff --git a/changelog/6755.bugfix.rst b/changelog/6755.bugfix.rst deleted file mode 100644 index 8077baa4f55..00000000000 --- a/changelog/6755.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Support deleting paths longer than 260 characters on windows created inside tmpdir. diff --git a/changelog/6817.improvement.rst b/changelog/6817.improvement.rst deleted file mode 100644 index 8d7e30d3467..00000000000 --- a/changelog/6817.improvement.rst +++ /dev/null @@ -1,2 +0,0 @@ -Explicit new-lines in help texts of command-line options are preserved, allowing plugins better control -of the help displayed to users. diff --git a/changelog/6856.feature.rst b/changelog/6856.feature.rst deleted file mode 100644 index 36892fa21b8..00000000000 --- a/changelog/6856.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -A warning is now shown when an unknown key is read from a config INI file. - -The `--strict-config` flag has been added to treat these warnings as errors. diff --git a/changelog/6871.bugfix.rst b/changelog/6871.bugfix.rst deleted file mode 100644 index fe69c750915..00000000000 --- a/changelog/6871.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix crash with captured output when using the :fixture:`capsysbinary fixture `. diff --git a/changelog/6903.breaking.rst b/changelog/6903.breaking.rst deleted file mode 100644 index a074a4ffecb..00000000000 --- a/changelog/6903.breaking.rst +++ /dev/null @@ -1,2 +0,0 @@ -The ``os.dup()`` function is now assumed to exist. We are not aware of any -supported Python 3 implementations which do not provide it. diff --git a/changelog/6906.feature.rst b/changelog/6906.feature.rst deleted file mode 100644 index 3e1fe3ef175..00000000000 --- a/changelog/6906.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added `--code-highlight` command line option to enable/disable code highlighting in terminal output. diff --git a/changelog/6909.bugfix.rst b/changelog/6909.bugfix.rst deleted file mode 100644 index 32edc4974c2..00000000000 --- a/changelog/6909.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Revert the change introduced by `#6330 `_, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature. - -The intention of the original change was to remove what was expected to be an unintended/surprising behavior, but it turns out many people relied on it, so the restriction has been reverted. diff --git a/changelog/6910.bugfix.rst b/changelog/6910.bugfix.rst deleted file mode 100644 index 713824998d1..00000000000 --- a/changelog/6910.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix crash when plugins return an unknown stats while using the ``--reportlog`` option. diff --git a/changelog/6924.bugfix.rst b/changelog/6924.bugfix.rst deleted file mode 100644 index 7283370a0e1..00000000000 --- a/changelog/6924.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Ensure a ``unittest.IsolatedAsyncioTestCase`` is actually awaited. diff --git a/changelog/6925.bugfix.rst b/changelog/6925.bugfix.rst deleted file mode 100644 index ed7e99b5dd2..00000000000 --- a/changelog/6925.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix TerminalRepr instances to be hashable again. diff --git a/changelog/6940.improvement.rst b/changelog/6940.improvement.rst deleted file mode 100644 index ab5fc0d49bf..00000000000 --- a/changelog/6940.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -When using the ``--duration`` option, the terminal message output is now more precise about the number and durations of hidden items. diff --git a/changelog/6947.bugfix.rst b/changelog/6947.bugfix.rst deleted file mode 100644 index 3168df8434c..00000000000 --- a/changelog/6947.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix regression where functions registered with ``TestCase.addCleanup`` were not being called on test failures. diff --git a/changelog/6951.bugfix.rst b/changelog/6951.bugfix.rst deleted file mode 100644 index 984089b3a56..00000000000 --- a/changelog/6951.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Allow users to still set the deprecated ``TerminalReporter.writer`` attribute. diff --git a/changelog/6956.bugfix.rst b/changelog/6956.bugfix.rst deleted file mode 100644 index a88ef94b6d5..00000000000 --- a/changelog/6956.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Prevent pytest from printing ConftestImportFailure traceback to stdout. diff --git a/changelog/6981.deprecation.rst b/changelog/6981.deprecation.rst deleted file mode 100644 index ac32706faec..00000000000 --- a/changelog/6981.deprecation.rst +++ /dev/null @@ -1 +0,0 @@ -Deprecate the ``pytest.collect`` module as it's just aliases into ``pytest``. diff --git a/changelog/6991.bugfix.rst b/changelog/6991.bugfix.rst deleted file mode 100644 index 879354e2533..00000000000 --- a/changelog/6991.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix regressions with `--lf` filtering too much since pytest 5.4. diff --git a/changelog/6991.improvement.rst b/changelog/6991.improvement.rst deleted file mode 100644 index ec08b66c27e..00000000000 --- a/changelog/6991.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Collected files are displayed after any reports from hooks, e.g. the status from ``--lf``. diff --git a/changelog/6992.bugfix.rst b/changelog/6992.bugfix.rst deleted file mode 100644 index 2c9b0f89eea..00000000000 --- a/changelog/6992.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Revert "tmpdir: clean up indirection via config for factories" #6767 as it breaks pytest-xdist. diff --git a/changelog/7035.trivial.rst b/changelog/7035.trivial.rst deleted file mode 100644 index 076cb4b4bcd..00000000000 --- a/changelog/7035.trivial.rst +++ /dev/null @@ -1,2 +0,0 @@ -The ``originalname`` attribute of ``_pytest.python.Function`` now defaults to ``name`` if not -provided explicitly, and is always set. diff --git a/changelog/7040.breaking.rst b/changelog/7040.breaking.rst deleted file mode 100644 index 897b2c0a972..00000000000 --- a/changelog/7040.breaking.rst +++ /dev/null @@ -1,6 +0,0 @@ -``-k`` no longer matches against the names of the directories outside the test session root. - -Also, ``pytest.Package.name`` is now just the name of the directory containing the package's -``__init__.py`` file, instead of the full path. This is consistent with how the other nodes -are named, and also one of the reasons why ``-k`` would match against any directory containing -the test suite. diff --git a/changelog/7061.bugfix.rst b/changelog/7061.bugfix.rst deleted file mode 100644 index 7e6d36c2fb1..00000000000 --- a/changelog/7061.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -When a yielding fixture fails to yield a value, report a test setup error instead of crashing. diff --git a/changelog/7076.bugfix.rst b/changelog/7076.bugfix.rst deleted file mode 100644 index 5d9749c69c4..00000000000 --- a/changelog/7076.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -The path of file skipped by ``@pytest.mark.skip`` in the SKIPPED report is now relative to invocation directory. Previously it was relative to root directory. diff --git a/changelog/7091.improvement.rst b/changelog/7091.improvement.rst deleted file mode 100644 index 72f17c5e48e..00000000000 --- a/changelog/7091.improvement.rst +++ /dev/null @@ -1,4 +0,0 @@ -When ``fd`` capturing is used, through ``--capture=fd`` or the ``capfd`` and -``capfdbinary`` fixtures, and the file descriptor (0, 1, 2) cannot be -duplicated, FD capturing is still performed. Previously, direct writes to the -file descriptors would fail or be lost in this case. diff --git a/changelog/7097.deprecation.rst b/changelog/7097.deprecation.rst deleted file mode 100644 index ed9779e1f50..00000000000 --- a/changelog/7097.deprecation.rst +++ /dev/null @@ -1,6 +0,0 @@ -The ``pytest._fillfuncargs`` function is now deprecated. This function was kept -for backward compatibility with an older plugin. - -It's functionality is not meant to be used directly, but if you must replace -it, use `function._request._fillfixtures()` instead, though note this is not -a public API and may break in the future. diff --git a/changelog/7110.bugfix.rst b/changelog/7110.bugfix.rst deleted file mode 100644 index 935f6ea3c85..00000000000 --- a/changelog/7110.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed regression: ``asyncbase.TestCase`` tests are executed correctly again. diff --git a/changelog/7119.improvement.rst b/changelog/7119.improvement.rst deleted file mode 100644 index 6cef9883633..00000000000 --- a/changelog/7119.improvement.rst +++ /dev/null @@ -1,2 +0,0 @@ -Exit with an error if the ``--basetemp`` argument is empty, the current working directory or parent directory of it. -This is done to protect against accidental data loss, as any directory passed to this argument is cleared. diff --git a/changelog/7122.breaking.rst b/changelog/7122.breaking.rst deleted file mode 100644 index 7fe329c9ff6..00000000000 --- a/changelog/7122.breaking.rst +++ /dev/null @@ -1,3 +0,0 @@ -Expressions given to the ``-m`` and ``-k`` options are no longer evaluated using Python's ``eval()``. -The format supports ``or``, ``and``, ``not``, parenthesis and general identifiers to match against. -Python constants, keywords or other operators are no longer evaluated differently. diff --git a/changelog/7126.bugfix.rst b/changelog/7126.bugfix.rst deleted file mode 100644 index 1a85e899781..00000000000 --- a/changelog/7126.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -``--setup-show`` now doesn't raise an error when a bytes value is used as a ``parametrize`` -parameter when Python is called with the ``-bb`` flag. diff --git a/changelog/7128.improvement.rst b/changelog/7128.improvement.rst deleted file mode 100644 index 9d24d567aed..00000000000 --- a/changelog/7128.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -`pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. diff --git a/changelog/7133.improvement.rst b/changelog/7133.improvement.rst deleted file mode 100644 index b537d3e5d6c..00000000000 --- a/changelog/7133.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``caplog.set_level()`` will now override any :confval:`log_level` set via the CLI or ``.ini``. diff --git a/changelog/7135.breaking.rst b/changelog/7135.breaking.rst deleted file mode 100644 index 4d5df4e402a..00000000000 --- a/changelog/7135.breaking.rst +++ /dev/null @@ -1,15 +0,0 @@ -Pytest now uses its own ``TerminalWriter`` class instead of using the one from the ``py`` library. -Plugins generally access this class through ``TerminalReporter.writer``, ``TerminalReporter.write()`` -(and similar methods), or ``_pytest.config.create_terminal_writer()``. - -The following breaking changes were made: - -- Output (``write()`` method and others) no longer flush implicitly; the flushing behavior - of the underlying file is respected. To flush explicitly (for example, if you - want output to be shown before an end-of-line is printed), use ``write(flush=True)`` or - ``terminal_writer.flush()``. -- Explicit Windows console support was removed, delegated to the colorama library. -- Support for writing ``bytes`` was removed. -- The ``reline`` method and ``chars_on_current_line`` property were removed. -- The ``stringio`` and ``encoding`` arguments was removed. -- Support for passing a callable instead of a file was removed. diff --git a/changelog/7143.bugfix.rst b/changelog/7143.bugfix.rst deleted file mode 100644 index abf47dd0c2d..00000000000 --- a/changelog/7143.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``File.from_constructor`` so it forwards extra keyword arguments to the constructor. diff --git a/changelog/7145.bugfix.rst b/changelog/7145.bugfix.rst deleted file mode 100644 index def237dc0c9..00000000000 --- a/changelog/7145.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Classes with broken ``__getattribute__`` methods are displayed correctly during failures. diff --git a/changelog/7150.bugfix.rst b/changelog/7150.bugfix.rst deleted file mode 100644 index 42cf5c7d2b9..00000000000 --- a/changelog/7150.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Prevent hiding the underlying exception when ``ConfTestImportFailure`` is raised. diff --git a/changelog/7159.improvement.rst b/changelog/7159.improvement.rst deleted file mode 100644 index c5f51a7b721..00000000000 --- a/changelog/7159.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -When the ``caplog`` fixture is used to change the log level for capturing, -using ``caplog.set_level()`` or ``caplog.at_level()``, it no longer affects -the level of logs that are shown in the "Captured log report" report section. diff --git a/changelog/7180.bugfix.rst b/changelog/7180.bugfix.rst deleted file mode 100644 index d2dd55e9b83..00000000000 --- a/changelog/7180.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``_is_setup_py`` for files encoded differently than locale. diff --git a/changelog/7202.doc.rst b/changelog/7202.doc.rst deleted file mode 100644 index 143f28d4080..00000000000 --- a/changelog/7202.doc.rst +++ /dev/null @@ -1 +0,0 @@ -The development guide now links to the contributing section of the docs and 'RELEASING.rst' on GitHub. diff --git a/changelog/7210.deprecation.rst b/changelog/7210.deprecation.rst deleted file mode 100644 index 3e1350eaa79..00000000000 --- a/changelog/7210.deprecation.rst +++ /dev/null @@ -1,5 +0,0 @@ -The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'`` -instead. - -The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue -if you use this and want a replacement. diff --git a/changelog/7215.bugfix.rst b/changelog/7215.bugfix.rst deleted file mode 100644 index 81514913285..00000000000 --- a/changelog/7215.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix regression where running with ``--pdb`` would call the ``tearDown`` methods of ``unittest.TestCase`` -subclasses for skipped tests. diff --git a/changelog/7224.breaking.rst b/changelog/7224.breaking.rst deleted file mode 100644 index 32592695aef..00000000000 --- a/changelog/7224.breaking.rst +++ /dev/null @@ -1,4 +0,0 @@ -The `item.catch_log_handler` and `item.catch_log_handlers` attributes, set by the -logging plugin and never meant to be public , are no longer available. - -The deprecated ``--no-print-logs`` option is removed. Use ``--show-capture`` instead. diff --git a/changelog/7226.breaking.rst b/changelog/7226.breaking.rst deleted file mode 100644 index bf1c443fc18..00000000000 --- a/changelog/7226.breaking.rst +++ /dev/null @@ -1 +0,0 @@ -Removed the unused ``args`` parameter from ``pytest.Function.__init__``. diff --git a/changelog/7233.doc.rst b/changelog/7233.doc.rst deleted file mode 100644 index c57f4d61f12..00000000000 --- a/changelog/7233.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add a note about ``--strict`` and ``--strict-markers`` and the preference for the latter one. diff --git a/changelog/7245.feature.rst b/changelog/7245.feature.rst deleted file mode 100644 index 05c3a6c0469..00000000000 --- a/changelog/7245.feature.rst +++ /dev/null @@ -1,14 +0,0 @@ -New ``--import-mode=importlib`` option that uses `importlib `__ to import test modules. - -Traditionally pytest used ``__import__`` while changing ``sys.path`` to import test modules (which -also changes ``sys.modules`` as a side-effect), which works but has a number of drawbacks, like requiring test modules -that don't live in packages to have unique names (as they need to reside under a unique name in ``sys.modules``). - -``--import-mode=importlib`` uses more fine grained import mechanisms from ``importlib`` which don't -require pytest to change ``sys.path`` or ``sys.modules`` at all, eliminating much of the drawbacks -of the previous mode. - -We intend to make ``--import-mode=importlib`` the default in future versions, so users are encouraged -to try the new mode and provide feedback (both positive or negative) in issue `#7245 `__. - -You can read more about this option in `the documentation `__. diff --git a/changelog/7253.bugfix.rst b/changelog/7253.bugfix.rst deleted file mode 100644 index e73ef663f00..00000000000 --- a/changelog/7253.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -When using ``pytest.fixture`` on a function directly, as in ``pytest.fixture(func)``, -if the ``autouse`` or ``params`` arguments are also passed, the function is no longer -ignored, but is marked as a fixture. diff --git a/changelog/7264.improvement.rst b/changelog/7264.improvement.rst deleted file mode 100644 index 035745c4dd9..00000000000 --- a/changelog/7264.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -The dependency on the ``wcwidth`` package has been removed. diff --git a/changelog/7291.trivial.rst b/changelog/7291.trivial.rst deleted file mode 100644 index 8f41528aa56..00000000000 --- a/changelog/7291.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Replaced ``py.iniconfig`` with `iniconfig `__. diff --git a/changelog/7295.trivial.rst b/changelog/7295.trivial.rst deleted file mode 100644 index 113a7ee605f..00000000000 --- a/changelog/7295.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -``src/_pytest/config/__init__.py`` now uses the ``warnings`` module to report warnings instead of ``sys.stderr.write``. diff --git a/changelog/7305.feature.rst b/changelog/7305.feature.rst deleted file mode 100644 index 96b7f72eed9..00000000000 --- a/changelog/7305.feature.rst +++ /dev/null @@ -1 +0,0 @@ -New ``required_plugins`` configuration option allows the user to specify a list of plugins required for pytest to run. An error is raised if any required plugins are not found when running pytest. diff --git a/changelog/7345.doc.rst b/changelog/7345.doc.rst deleted file mode 100644 index 4c7234f4144..00000000000 --- a/changelog/7345.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Explain indirect parametrization and markers for fixtures diff --git a/changelog/7346.feature.rst b/changelog/7346.feature.rst deleted file mode 100644 index fef0bbb781c..00000000000 --- a/changelog/7346.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Version information as defined by `PEP 440 `_ may now be included when providing plugins to the ``required_plugins`` configuration option. diff --git a/changelog/7348.improvement.rst b/changelog/7348.improvement.rst deleted file mode 100644 index 714892e6e4e..00000000000 --- a/changelog/7348.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Improve recursive diff report for comparison asserts on dataclasses / attrs. diff --git a/changelog/7356.trivial.rst b/changelog/7356.trivial.rst deleted file mode 100644 index d280e229125..00000000000 --- a/changelog/7356.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Remove last internal uses of deprecated "slave" term from old pytest-xdist. diff --git a/changelog/7357.trivial.rst b/changelog/7357.trivial.rst deleted file mode 100644 index f0f9d035dfc..00000000000 --- a/changelog/7357.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -py>=1.8.2 is now required. diff --git a/changelog/7360.bugfix.rst b/changelog/7360.bugfix.rst deleted file mode 100644 index b84ce461473..00000000000 --- a/changelog/7360.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix possibly incorrect evaluation of string expressions passed to ``pytest.mark.skipif`` and ``pytest.mark.xfail``, -in rare circumstances where the exact same string is used but refers to different global values. diff --git a/changelog/7383.bugfix.rst b/changelog/7383.bugfix.rst deleted file mode 100644 index d43106880cc..00000000000 --- a/changelog/7383.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed exception causes all over the codebase, i.e. use `raise new_exception from old_exception` when wrapping an exception. diff --git a/changelog/7385.improvement.rst b/changelog/7385.improvement.rst deleted file mode 100644 index c02fee5dad1..00000000000 --- a/changelog/7385.improvement.rst +++ /dev/null @@ -1,13 +0,0 @@ -``--junitxml`` now includes the exception cause in the ``message`` XML attribute for failures during setup and teardown. - -Previously: - -.. code-block:: xml - - - -Now: - -.. code-block:: xml - - diff --git a/changelog/7418.breaking.rst b/changelog/7418.breaking.rst deleted file mode 100644 index 23f60da3765..00000000000 --- a/changelog/7418.breaking.rst +++ /dev/null @@ -1,2 +0,0 @@ -Remove the `pytest_doctest_prepare_content` hook specification. This hook -hasn't been triggered by pytest for at least 10 years. diff --git a/changelog/7438.breaking.rst b/changelog/7438.breaking.rst deleted file mode 100644 index 5d5d239fc99..00000000000 --- a/changelog/7438.breaking.rst +++ /dev/null @@ -1,11 +0,0 @@ -Some changes were made to the internal ``_pytest._code.source``, listed here -for the benefit of plugin authors who may be using it: - -- The ``deindent`` argument to ``Source()`` has been removed, now it is always true. -- Support for zero or multiple arguments to ``Source()`` has been removed. -- Support for comparing ``Source`` with an ``str`` has been removed. -- The methods ``Source.isparseable()`` and ``Source.putaround()`` have been removed. -- The method ``Source.compile()`` and function ``_pytest._code.compile()`` have - been removed; use plain ``compile()`` instead. -- The function ``_pytest._code.source.getsource()`` has been removed; use - ``Source()`` directly instead. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 4405e6fe04b..31deaad71c8 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.0.0rc1 release-5.4.3 release-5.4.2 release-5.4.1 diff --git a/doc/en/announce/release-6.0.0rc1.rst b/doc/en/announce/release-6.0.0rc1.rst new file mode 100644 index 00000000000..32193d5c136 --- /dev/null +++ b/doc/en/announce/release-6.0.0rc1.rst @@ -0,0 +1,69 @@ +pytest-6.0.0rc1 +======================================= + +pytest 6.0.0rc1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Alfredo Deza +* Andreas Maier +* Andrew +* Anthony Sottile +* ArtyomKaltovich +* Bruno Oliveira +* Claire Cecil +* Curt J. Sampson +* Daniel +* Daniel Hahler +* Danny Sepler +* David Diaz Barquero +* Fabio Zadrozny +* Felix Nieuwenhuizen +* Florian Bruhin +* Florian Dahlitz +* Gleb Nikonorov +* Hugo van Kemenade +* Hunter Richards +* Katarzyna +* Katarzyna Król +* Katrin Leinweber +* Keri Volans +* Lewis Belcher +* Lukas Geiger +* Martin Michlmayr +* Mattwmaster58 +* Maximilian Cosmo Sitter +* Nikolay Kondratyev +* Pavel Karateev +* Paweł Wilczyński +* Prashant Anand +* Ram Rachum +* Ran Benita +* Ronny Pfannschmidt +* Ruaridh Williamson +* Simon K +* Tim Hoffmann +* Tor Colvin +* Vlad-Radz +* Xinbin Huang +* Zac Hatfield-Dodds +* Zac-HD +* earonesty +* gaurav dhameeja +* gdhameeja +* ibriquem +* mcsitter +* piotrhm +* smarie +* symonk +* xuiqzy + + +Happy testing, +The pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 87c79720f19..e39da6e8a27 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -31,7 +31,7 @@ you will see the return value of the function call: $ pytest test_assert1.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item @@ -188,7 +188,7 @@ if you run this module: $ pytest test_assert2.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item diff --git a/doc/en/cache.rst b/doc/en/cache.rst index fa51bd5ee9f..076d4b1973c 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -86,7 +86,7 @@ If you then run it with ``--lf``: $ pytest --lf =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 2 items @@ -133,7 +133,7 @@ of ``FF`` and dots): $ pytest --ff =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 50 items @@ -277,7 +277,7 @@ You can always peek at the content of the cache using the $ pytest --cache-show =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR cachedir: $PYTHON_PREFIX/.pytest_cache @@ -290,19 +290,7 @@ You can always peek at the content of the cache using the 'test_caching.py::test_function': True, 'test_foocompare.py::test_compare': True} cache/nodeids contains: - ['test_assert1.py::test_function', - 'test_assert2.py::test_set_comparison', - 'test_foocompare.py::test_compare', - 'test_50.py::test_num[0]', - 'test_50.py::test_num[1]', - 'test_50.py::test_num[2]', - 'test_50.py::test_num[3]', - 'test_50.py::test_num[4]', - 'test_50.py::test_num[5]', - 'test_50.py::test_num[6]', - 'test_50.py::test_num[7]', - 'test_50.py::test_num[8]', - 'test_50.py::test_num[9]', + ['test_50.py::test_num[0]', 'test_50.py::test_num[10]', 'test_50.py::test_num[11]', 'test_50.py::test_num[12]', @@ -313,6 +301,7 @@ You can always peek at the content of the cache using the 'test_50.py::test_num[17]', 'test_50.py::test_num[18]', 'test_50.py::test_num[19]', + 'test_50.py::test_num[1]', 'test_50.py::test_num[20]', 'test_50.py::test_num[21]', 'test_50.py::test_num[22]', @@ -323,6 +312,7 @@ You can always peek at the content of the cache using the 'test_50.py::test_num[27]', 'test_50.py::test_num[28]', 'test_50.py::test_num[29]', + 'test_50.py::test_num[2]', 'test_50.py::test_num[30]', 'test_50.py::test_num[31]', 'test_50.py::test_num[32]', @@ -333,6 +323,7 @@ You can always peek at the content of the cache using the 'test_50.py::test_num[37]', 'test_50.py::test_num[38]', 'test_50.py::test_num[39]', + 'test_50.py::test_num[3]', 'test_50.py::test_num[40]', 'test_50.py::test_num[41]', 'test_50.py::test_num[42]', @@ -343,7 +334,16 @@ You can always peek at the content of the cache using the 'test_50.py::test_num[47]', 'test_50.py::test_num[48]', 'test_50.py::test_num[49]', - 'test_caching.py::test_function'] + 'test_50.py::test_num[4]', + 'test_50.py::test_num[5]', + 'test_50.py::test_num[6]', + 'test_50.py::test_num[7]', + 'test_50.py::test_num[8]', + 'test_50.py::test_num[9]', + 'test_assert1.py::test_function', + 'test_assert2.py::test_set_comparison', + 'test_caching.py::test_function', + 'test_foocompare.py::test_compare'] cache/stepwise contains: [] example/value contains: @@ -358,7 +358,7 @@ filtering: $ pytest --cache-show example/* =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR cachedir: $PYTHON_PREFIX/.pytest_cache diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 44d3a3bd175..caaebdf81a8 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -83,7 +83,7 @@ of the failing function and hide the other one: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 2 items diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index ab622ec1e4f..d6e07c8e4e9 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,423 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 6.0.0rc1 (2020-07-08) +============================ + +Breaking Changes +---------------- + +- `#1316 `_: ``TestReport.longrepr`` is now always an instance of ``ReprExceptionInfo``. Previously it was a ``str`` when a test failed with ``pytest.fail(..., pytrace=False)``. + + +- `#5965 `_: symlinks are no longer resolved during collection and matching `conftest.py` files with test file paths. + + Resolving symlinks for the current directory and during collection was introduced as a bugfix in 3.9.0, but it actually is a new feature which had unfortunate consequences in Windows and surprising results in other platforms. + + The team decided to step back on resolving symlinks at all, planning to review this in the future with a more solid solution (see discussion in + `#6523 `__ for details). + + This might break test suites which made use of this feature; the fix is to create a symlink + for the entire test tree, and not only to partial files/tress as it was possible previously. + + +- `#6505 `_: ``Testdir.run().parseoutcomes()`` now always returns the parsed nouns in plural form. + + Originally ``parseoutcomes()`` would always returns the nouns in plural form, but a change + meant to improve the terminal summary by using singular form single items (``1 warning`` or ``1 error``) + caused an unintended regression by changing the keys returned by ``parseoutcomes()``. + + Now the API guarantees to always return the plural form, so calls like this: + + .. code-block:: python + + result = testdir.runpytest() + result.assert_outcomes(error=1) + + Need to be changed to: + + + .. code-block:: python + + result = testdir.runpytest() + result.assert_outcomes(errors=1) + + +- `#6903 `_: The ``os.dup()`` function is now assumed to exist. We are not aware of any + supported Python 3 implementations which do not provide it. + + +- `#7040 `_: ``-k`` no longer matches against the names of the directories outside the test session root. + + Also, ``pytest.Package.name`` is now just the name of the directory containing the package's + ``__init__.py`` file, instead of the full path. This is consistent with how the other nodes + are named, and also one of the reasons why ``-k`` would match against any directory containing + the test suite. + + +- `#7122 `_: Expressions given to the ``-m`` and ``-k`` options are no longer evaluated using Python's ``eval()``. + The format supports ``or``, ``and``, ``not``, parenthesis and general identifiers to match against. + Python constants, keywords or other operators are no longer evaluated differently. + + +- `#7135 `_: Pytest now uses its own ``TerminalWriter`` class instead of using the one from the ``py`` library. + Plugins generally access this class through ``TerminalReporter.writer``, ``TerminalReporter.write()`` + (and similar methods), or ``_pytest.config.create_terminal_writer()``. + + The following breaking changes were made: + + - Output (``write()`` method and others) no longer flush implicitly; the flushing behavior + of the underlying file is respected. To flush explicitly (for example, if you + want output to be shown before an end-of-line is printed), use ``write(flush=True)`` or + ``terminal_writer.flush()``. + - Explicit Windows console support was removed, delegated to the colorama library. + - Support for writing ``bytes`` was removed. + - The ``reline`` method and ``chars_on_current_line`` property were removed. + - The ``stringio`` and ``encoding`` arguments was removed. + - Support for passing a callable instead of a file was removed. + + +- `#7224 `_: The `item.catch_log_handler` and `item.catch_log_handlers` attributes, set by the + logging plugin and never meant to be public , are no longer available. + + The deprecated ``--no-print-logs`` option is removed. Use ``--show-capture`` instead. + + +- `#7226 `_: Removed the unused ``args`` parameter from ``pytest.Function.__init__``. + + +- `#7418 `_: Remove the `pytest_doctest_prepare_content` hook specification. This hook + hasn't been triggered by pytest for at least 10 years. + + +- `#7438 `_: Some changes were made to the internal ``_pytest._code.source``, listed here + for the benefit of plugin authors who may be using it: + + - The ``deindent`` argument to ``Source()`` has been removed, now it is always true. + - Support for zero or multiple arguments to ``Source()`` has been removed. + - Support for comparing ``Source`` with an ``str`` has been removed. + - The methods ``Source.isparseable()`` and ``Source.putaround()`` have been removed. + - The method ``Source.compile()`` and function ``_pytest._code.compile()`` have + been removed; use plain ``compile()`` instead. + - The function ``_pytest._code.source.getsource()`` has been removed; use + ``Source()`` directly instead. + + + +Deprecations +------------ + +- `#6981 `_: Deprecate the ``pytest.collect`` module as it's just aliases into ``pytest``. + + +- `#7097 `_: The ``pytest._fillfuncargs`` function is now deprecated. This function was kept + for backward compatibility with an older plugin. + + It's functionality is not meant to be used directly, but if you must replace + it, use `function._request._fillfixtures()` instead, though note this is not + a public API and may break in the future. + + +- `#7210 `_: The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'`` + instead. + + The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue + if you use this and want a replacement. + + + +Features +-------- + +- `#1556 `_: pytest now supports ``pyproject.toml`` files for configuration. + + The configuration options is similar to the one available in other formats, but must be defined + in a ``[tool.pytest.ini_options]`` table to be picked up by pytest: + + .. code-block:: toml + + # pyproject.toml + [tool.pytest.ini_options] + minversion = "6.0" + addopts = "-ra -q" + testpaths = [ + "tests", + "integration", + ] + + More information can be found `in the docs `__. + + +- `#3342 `_: pytest now includes inline type annotations and exposes them to user programs. + Most of the user-facing API is covered, as well as internal code. + + If you are running a type checker such as mypy on your tests, you may start + noticing type errors indicating incorrect usage. If you run into an error that + you believe to be incorrect, please let us know in an issue. + + The types were developed against mypy version 0.780. Older versions may work, + but we recommend using at least this version. Other type checkers may work as + well, but they are not officially verified to work by pytest yet. + + +- `#4049 `_: Introduced a new hook named `pytest_warning_recorded` to convey information about warnings captured by the internal `pytest` warnings plugin. + + This hook is meant to replace `pytest_warning_captured`, which will be removed in a future release. + + +- `#6285 `_: Exposed the `pytest.FixtureLookupError` exception which is raised by `request.getfixturevalue()` + (where `request` is a `FixtureRequest` fixture) when a fixture with the given name cannot be returned. + + +- `#6433 `_: If an error is encountered while formatting the message in a logging call, for + example ``logging.warning("oh no!: %s: %s", "first")`` (a second argument is + missing), pytest now propagates the error, likely causing the test to fail. + + Previously, such a mistake would cause an error to be printed to stderr, which + is not displayed by default for passing tests. This change makes the mistake + visible during testing. + + You may supress this behavior temporarily or permanently by setting + ``logging.raiseExceptions = False``. + + +- `#6471 `_: New command-line flags: + + * `--no-header`: disables the initial header, including platform, version, and plugins. + * `--no-summary`: disables the final test summary, including warnings. + + +- `#6856 `_: A warning is now shown when an unknown key is read from a config INI file. + + The `--strict-config` flag has been added to treat these warnings as errors. + + +- `#6906 `_: Added `--code-highlight` command line option to enable/disable code highlighting in terminal output. + + +- `#7245 `_: New ``--import-mode=importlib`` option that uses `importlib `__ to import test modules. + + Traditionally pytest used ``__import__`` while changing ``sys.path`` to import test modules (which + also changes ``sys.modules`` as a side-effect), which works but has a number of drawbacks, like requiring test modules + that don't live in packages to have unique names (as they need to reside under a unique name in ``sys.modules``). + + ``--import-mode=importlib`` uses more fine grained import mechanisms from ``importlib`` which don't + require pytest to change ``sys.path`` or ``sys.modules`` at all, eliminating much of the drawbacks + of the previous mode. + + We intend to make ``--import-mode=importlib`` the default in future versions, so users are encouraged + to try the new mode and provide feedback (both positive or negative) in issue `#7245 `__. + + You can read more about this option in `the documentation `__. + + +- `#7305 `_: New ``required_plugins`` configuration option allows the user to specify a list of plugins required for pytest to run. An error is raised if any required plugins are not found when running pytest. + + +- `#7346 `_: Version information as defined by `PEP 440 `_ may now be included when providing plugins to the ``required_plugins`` configuration option. + + + +Improvements +------------ + +- `#4375 `_: The ``pytest`` command now supresses the ``BrokenPipeError`` error message that + is printed to stderr when the output of ``pytest`` is piped and and the pipe is + closed by the piped-to program (common examples are ``less`` and ``head``). + + +- `#4391 `_: Improved precision of test durations measurement. ``CallInfo`` items now have a new ``.duration`` attribute, created using ``time.perf_counter()``. This attribute is used to fill the ``.duration`` attribute, which is more accurate than the previous ``.stop - .start`` (as these are based on ``time.time()``). + + +- `#4675 `_: Rich comparison for dataclasses and `attrs`-classes is now recursive. + + +- `#6817 `_: Explicit new-lines in help texts of command-line options are preserved, allowing plugins better control + of the help displayed to users. + + +- `#6940 `_: When using the ``--duration`` option, the terminal message output is now more precise about the number and durations of hidden items. + + +- `#6991 `_: Collected files are displayed after any reports from hooks, e.g. the status from ``--lf``. + + +- `#7091 `_: When ``fd`` capturing is used, through ``--capture=fd`` or the ``capfd`` and + ``capfdbinary`` fixtures, and the file descriptor (0, 1, 2) cannot be + duplicated, FD capturing is still performed. Previously, direct writes to the + file descriptors would fail or be lost in this case. + + +- `#7119 `_: Exit with an error if the ``--basetemp`` argument is empty, the current working directory or parent directory of it. + This is done to protect against accidental data loss, as any directory passed to this argument is cleared. + + +- `#7128 `_: `pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. + + +- `#7133 `_: ``caplog.set_level()`` will now override any :confval:`log_level` set via the CLI or ``.ini``. + + +- `#7159 `_: When the ``caplog`` fixture is used to change the log level for capturing, + using ``caplog.set_level()`` or ``caplog.at_level()``, it no longer affects + the level of logs that are shown in the "Captured log report" report section. + + +- `#7264 `_: The dependency on the ``wcwidth`` package has been removed. + + +- `#7348 `_: Improve recursive diff report for comparison asserts on dataclasses / attrs. + + +- `#7385 `_: ``--junitxml`` now includes the exception cause in the ``message`` XML attribute for failures during setup and teardown. + + Previously: + + .. code-block:: xml + + + + Now: + + .. code-block:: xml + + + + + +Bug Fixes +--------- + +- `#1120 `_: Fix issue where directories from tmpdir are not removed properly when multiple instances of pytest are running in parallel. + + +- `#4583 `_: Prevent crashing and provide a user-friendly error when a marker expression (-m) invoking of eval() raises any exception. + + +- `#4677 `_: The path shown in the summary report for SKIPPED tests is now always relative. Previously it was sometimes absolute. + + +- `#5456 `_: Fix a possible race condition when trying to remove lock files used to control access to folders + created by ``tmp_path`` and ``tmpdir``. + + +- `#6240 `_: Fixes an issue where logging during collection step caused duplication of log + messages to stderr. + + +- `#6428 `_: Paths appearing in error messages are now correct in case the current working directory has + changed since the start of the session. + + +- `#6755 `_: Support deleting paths longer than 260 characters on windows created inside tmpdir. + + +- `#6871 `_: Fix crash with captured output when using the :fixture:`capsysbinary fixture `. + + +- `#6909 `_: Revert the change introduced by `#6330 `_, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature. + + The intention of the original change was to remove what was expected to be an unintended/surprising behavior, but it turns out many people relied on it, so the restriction has been reverted. + + +- `#6910 `_: Fix crash when plugins return an unknown stats while using the ``--reportlog`` option. + + +- `#6924 `_: Ensure a ``unittest.IsolatedAsyncioTestCase`` is actually awaited. + + +- `#6925 `_: Fix TerminalRepr instances to be hashable again. + + +- `#6947 `_: Fix regression where functions registered with ``TestCase.addCleanup`` were not being called on test failures. + + +- `#6951 `_: Allow users to still set the deprecated ``TerminalReporter.writer`` attribute. + + +- `#6956 `_: Prevent pytest from printing ConftestImportFailure traceback to stdout. + + +- `#6991 `_: Fix regressions with `--lf` filtering too much since pytest 5.4. + + +- `#6992 `_: Revert "tmpdir: clean up indirection via config for factories" #6767 as it breaks pytest-xdist. + + +- `#7061 `_: When a yielding fixture fails to yield a value, report a test setup error instead of crashing. + + +- `#7076 `_: The path of file skipped by ``@pytest.mark.skip`` in the SKIPPED report is now relative to invocation directory. Previously it was relative to root directory. + + +- `#7110 `_: Fixed regression: ``asyncbase.TestCase`` tests are executed correctly again. + + +- `#7126 `_: ``--setup-show`` now doesn't raise an error when a bytes value is used as a ``parametrize`` + parameter when Python is called with the ``-bb`` flag. + + +- `#7143 `_: Fix ``File.from_constructor`` so it forwards extra keyword arguments to the constructor. + + +- `#7145 `_: Classes with broken ``__getattribute__`` methods are displayed correctly during failures. + + +- `#7150 `_: Prevent hiding the underlying exception when ``ConfTestImportFailure`` is raised. + + +- `#7180 `_: Fix ``_is_setup_py`` for files encoded differently than locale. + + +- `#7215 `_: Fix regression where running with ``--pdb`` would call the ``tearDown`` methods of ``unittest.TestCase`` + subclasses for skipped tests. + + +- `#7253 `_: When using ``pytest.fixture`` on a function directly, as in ``pytest.fixture(func)``, + if the ``autouse`` or ``params`` arguments are also passed, the function is no longer + ignored, but is marked as a fixture. + + +- `#7360 `_: Fix possibly incorrect evaluation of string expressions passed to ``pytest.mark.skipif`` and ``pytest.mark.xfail``, + in rare circumstances where the exact same string is used but refers to different global values. + + +- `#7383 `_: Fixed exception causes all over the codebase, i.e. use `raise new_exception from old_exception` when wrapping an exception. + + + +Improved Documentation +---------------------- + +- `#7202 `_: The development guide now links to the contributing section of the docs and 'RELEASING.rst' on GitHub. + + +- `#7233 `_: Add a note about ``--strict`` and ``--strict-markers`` and the preference for the latter one. + + +- `#7345 `_: Explain indirect parametrization and markers for fixtures + + + +Trivial/Internal Changes +------------------------ + +- `#7035 `_: The ``originalname`` attribute of ``_pytest.python.Function`` now defaults to ``name`` if not + provided explicitly, and is always set. + + +- `#7291 `_: Replaced ``py.iniconfig`` with `iniconfig `__. + + +- `#7295 `_: ``src/_pytest/config/__init__.py`` now uses the ``warnings`` module to report warnings instead of ``sys.stderr.write``. + + +- `#7356 `_: Remove last internal uses of deprecated "slave" term from old pytest-xdist. + + +- `#7357 `_: py>=1.8.2 is now required. + + pytest 5.4.3 (2020-06-02) ========================= diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index a85ac3d6442..bb96ee40925 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -29,7 +29,7 @@ then you can just invoke ``pytest`` directly: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item @@ -58,7 +58,7 @@ and functions, including from test modules: $ pytest --doctest-modules =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 2 items diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 55dd8af234f..454304679aa 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -45,7 +45,7 @@ You can then restrict a test run to only run tests marked with ``webtest``: $ pytest -v -m webtest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 3 deselected / 1 selected @@ -60,7 +60,7 @@ Or the inverse, running all tests except the webtest ones: $ pytest -v -m "not webtest" =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 1 deselected / 3 selected @@ -82,7 +82,7 @@ tests based on their module, class, method, or function name: $ pytest -v test_server.py::TestClass::test_method =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 1 item @@ -97,7 +97,7 @@ You can also select on the class: $ pytest -v test_server.py::TestClass =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 1 item @@ -112,7 +112,7 @@ Or select multiple nodes: $ pytest -v test_server.py::TestClass test_server.py::test_send_http =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 2 items @@ -156,7 +156,7 @@ The expression matching is now case-insensitive. $ pytest -v -k http # running with the above defined example module =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 3 deselected / 1 selected @@ -171,7 +171,7 @@ And you can also run all tests except the ones that match the keyword: $ pytest -k "not send_http" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 1 deselected / 3 selected @@ -188,7 +188,7 @@ Or to select "http" and "quick" tests: $ pytest -k "http or quick" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 4 items / 2 deselected / 2 selected @@ -228,9 +228,9 @@ You can ask which markers exist for your test suite - the list includes our just @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test. - @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see https://docs.pytest.org/en/stable/skipping.html + @pytest.mark.skipif(condition, ..., *, reason=...): skip the given test function if any of the conditions evaluate to True. Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. See https://docs.pytest.org/en/stable/reference.html#pytest-mark-skipif - @pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False): mark the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/stable/skipping.html + @pytest.mark.xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): mark the test function as an expected failure if any of the conditions evaluate to True. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/stable/reference.html#pytest-mark-xfail @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/stable/parametrize.html for more info and examples. @@ -398,7 +398,7 @@ the test needs: $ pytest -E stage2 =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item @@ -413,7 +413,7 @@ and here is one that specifies exactly the environment needed: $ pytest -E stage1 =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item @@ -433,9 +433,9 @@ The ``--markers`` option always gives you a list of available markers: @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test. - @pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see https://docs.pytest.org/en/stable/skipping.html + @pytest.mark.skipif(condition, ..., *, reason=...): skip the given test function if any of the conditions evaluate to True. Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. See https://docs.pytest.org/en/stable/reference.html#pytest-mark-skipif - @pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False): mark the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/stable/skipping.html + @pytest.mark.xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): mark the test function as an expected failure if any of the conditions evaluate to True. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/stable/reference.html#pytest-mark-xfail @pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/stable/parametrize.html for more info and examples. @@ -606,7 +606,7 @@ then you will see two tests skipped and two executed tests as expected: $ pytest -rs # this option reports skip reasons =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 4 items @@ -614,7 +614,7 @@ then you will see two tests skipped and two executed tests as expected: test_plat.py s.s. [100%] ========================= short test summary info ========================== - SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:12: cannot run on platform linux + SKIPPED [2] conftest.py:12: cannot run on platform linux ======================= 2 passed, 2 skipped in 0.12s ======================= Note that if you specify a platform via the marker-command line option like this: @@ -623,7 +623,7 @@ Note that if you specify a platform via the marker-command line option like this $ pytest -m linux =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 4 items / 3 deselected / 1 selected @@ -687,7 +687,7 @@ We can now use the ``-m option`` to select one set: $ pytest -m interface --tb=short =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 4 items / 2 deselected / 2 selected @@ -714,7 +714,7 @@ or to select both "event" and "interface" tests: $ pytest -m "interface or event" --tb=short =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 4 items / 1 deselected / 3 selected diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index 083f6b43912..d15b7ae8bdd 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -29,7 +29,7 @@ now execute the test specification: nonpython $ pytest test_simple.yaml =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR/nonpython collected 2 items @@ -66,7 +66,7 @@ consulted when reporting in ``verbose`` mode: nonpython $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR/nonpython collecting ... collected 2 items @@ -92,11 +92,12 @@ interesting to just look at the collection tree: nonpython $ pytest --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR/nonpython collected 2 items - + + diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 0b61a19bc91..8fa48bfe340 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -160,10 +160,11 @@ objects, they are still using the default pytest representation: $ pytest test_time.py --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 8 items + @@ -224,7 +225,7 @@ this is a fully self-contained example which you can run with: $ pytest test_scenarios.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 4 items @@ -239,10 +240,11 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia $ pytest --collect-only test_scenarios.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 4 items + @@ -317,10 +319,11 @@ Let's first see how it looks like at collection time: $ pytest test_backends.py --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 2 items + @@ -415,15 +418,14 @@ The result of this test will be successful: $ pytest -v test_indirect_list.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR - collected 1 item - - test_indirect_list.py::test_indirect[a-b] PASSED + collecting ... collected 1 item - ========================== 1 passed in 0.01s =============================== + test_indirect_list.py::test_indirect[a-b] PASSED [100%] + ============================ 1 passed in 0.12s ============================= .. regendoc:wipe @@ -506,10 +508,11 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ssssssssssss......sss...... [100%] + ssssssssssssssssssssssss... [100%] ========================= short test summary info ========================== - SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found - 12 passed, 15 skipped in 0.12s + SKIPPED [12] multipython.py:29: 'python3.5' not found + SKIPPED [12] multipython.py:29: 'python3.6' not found + 3 passed, 24 skipped in 0.12s Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- @@ -569,7 +572,7 @@ If you run this with reporting for skips enabled: $ pytest -rs test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 2 items @@ -577,7 +580,7 @@ If you run this with reporting for skips enabled: test_module.py .s [100%] ========================= short test summary info ========================== - SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:12: could not import 'opt2': No module named 'opt2' + SKIPPED [1] conftest.py:12: could not import 'opt2': No module named 'opt2' ======================= 1 passed, 1 skipped in 0.12s ======================= You'll see that we don't have an ``opt2`` module and thus the second test run @@ -631,7 +634,7 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker: $ pytest -v -m basic =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 14 items / 11 deselected / 3 selected diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 30d106adab6..85e5da26302 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -147,10 +147,11 @@ The test collection would look like this: $ pytest --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, configfile: pytest.ini collected 2 items + @@ -208,10 +209,11 @@ You can always peek at the collection tree without running tests like this: . $ pytest --collect-only pythoncollection.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, configfile: pytest.ini collected 3 items + @@ -289,9 +291,9 @@ file will be left out: $ pytest --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, configfile: pytest.ini collected 0 items ========================== no tests ran in 0.12s =========================== diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 23c302eca85..f1b973f3b33 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -9,7 +9,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: assertion $ pytest failure_demo.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR/assertion collected 44 items @@ -26,7 +26,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert param1 * 2 < param2 E assert (3 * 2) < 6 - failure_demo.py:20: AssertionError + failure_demo.py:19: AssertionError _________________________ TestFailing.test_simple __________________________ self = @@ -43,7 +43,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 42 = .f at 0xdeadbeef>() E + and 43 = .g at 0xdeadbeef>() - failure_demo.py:31: AssertionError + failure_demo.py:30: AssertionError ____________________ TestFailing.test_simple_multiline _____________________ self = @@ -51,7 +51,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_simple_multiline(self): > otherfunc_multi(42, 6 * 9) - failure_demo.py:34: + failure_demo.py:33: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ a = 42, b = 54 @@ -60,7 +60,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert a == b E assert 42 == 54 - failure_demo.py:15: AssertionError + failure_demo.py:14: AssertionError ___________________________ TestFailing.test_not ___________________________ self = @@ -73,7 +73,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert not 42 E + where 42 = .f at 0xdeadbeef>() - failure_demo.py:40: AssertionError + failure_demo.py:39: AssertionError _________________ TestSpecialisedExplanations.test_eq_text _________________ self = @@ -84,7 +84,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E - eggs E + spam - failure_demo.py:45: AssertionError + failure_demo.py:44: AssertionError _____________ TestSpecialisedExplanations.test_eq_similar_text _____________ self = @@ -97,7 +97,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + foo 1 bar E ? ^ - failure_demo.py:48: AssertionError + failure_demo.py:47: AssertionError ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ self = @@ -110,7 +110,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + spam E bar - failure_demo.py:51: AssertionError + failure_demo.py:50: AssertionError ______________ TestSpecialisedExplanations.test_eq_long_text _______________ self = @@ -127,7 +127,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + 1111111111a222222222 E ? ^ - failure_demo.py:56: AssertionError + failure_demo.py:55: AssertionError _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ self = @@ -147,7 +147,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (7 lines hidden), use '-vv' to show - failure_demo.py:61: AssertionError + failure_demo.py:60: AssertionError _________________ TestSpecialisedExplanations.test_eq_list _________________ self = @@ -158,7 +158,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E At index 2 diff: 2 != 3 E Use -v to get the full diff - failure_demo.py:64: AssertionError + failure_demo.py:63: AssertionError ______________ TestSpecialisedExplanations.test_eq_list_long _______________ self = @@ -171,7 +171,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E At index 100 diff: 1 != 2 E Use -v to get the full diff - failure_demo.py:69: AssertionError + failure_demo.py:68: AssertionError _________________ TestSpecialisedExplanations.test_eq_dict _________________ self = @@ -189,7 +189,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (2 lines hidden), use '-vv' to show - failure_demo.py:72: AssertionError + failure_demo.py:71: AssertionError _________________ TestSpecialisedExplanations.test_eq_set __________________ self = @@ -207,7 +207,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (2 lines hidden), use '-vv' to show - failure_demo.py:75: AssertionError + failure_demo.py:74: AssertionError _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ self = @@ -218,7 +218,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E Right contains one more item: 3 E Use -v to get the full diff - failure_demo.py:78: AssertionError + failure_demo.py:77: AssertionError _________________ TestSpecialisedExplanations.test_in_list _________________ self = @@ -227,7 +227,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert 1 in [0, 2, 3, 4, 5] E assert 1 in [0, 2, 3, 4, 5] - failure_demo.py:81: AssertionError + failure_demo.py:80: AssertionError __________ TestSpecialisedExplanations.test_not_in_text_multiline __________ self = @@ -246,7 +246,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (2 lines hidden), use '-vv' to show - failure_demo.py:85: AssertionError + failure_demo.py:84: AssertionError ___________ TestSpecialisedExplanations.test_not_in_text_single ____________ self = @@ -259,7 +259,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E single foo line E ? +++ - failure_demo.py:89: AssertionError + failure_demo.py:88: AssertionError _________ TestSpecialisedExplanations.test_not_in_text_single_long _________ self = @@ -272,7 +272,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? +++ - failure_demo.py:93: AssertionError + failure_demo.py:92: AssertionError ______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______ self = @@ -285,7 +285,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - failure_demo.py:97: AssertionError + failure_demo.py:96: AssertionError ______________ TestSpecialisedExplanations.test_eq_dataclass _______________ self = @@ -302,11 +302,17 @@ Here is a nice run of several failures and how ``pytest`` presents things: right = Foo(1, "c") > assert left == right E AssertionError: assert TestSpecialis...oo(a=1, b='b') == TestSpecialis...oo(a=1, b='c') + E E Omitting 1 identical items, use -vv to show E Differing attributes: - E b: 'b' != 'c' + E ['b'] + E + E Drill down into differing attribute b: + E b: 'b' != 'c'... + E + E ...Full output truncated (3 lines hidden), use '-vv' to show - failure_demo.py:109: AssertionError + failure_demo.py:108: AssertionError ________________ TestSpecialisedExplanations.test_eq_attrs _________________ self = @@ -323,11 +329,17 @@ Here is a nice run of several failures and how ``pytest`` presents things: right = Foo(1, "c") > assert left == right E AssertionError: assert Foo(a=1, b='b') == Foo(a=1, b='c') + E E Omitting 1 identical items, use -vv to show E Differing attributes: - E b: 'b' != 'c' + E ['b'] + E + E Drill down into differing attribute b: + E b: 'b' != 'c'... + E + E ...Full output truncated (3 lines hidden), use '-vv' to show - failure_demo.py:121: AssertionError + failure_demo.py:120: AssertionError ______________________________ test_attribute ______________________________ def test_attribute(): @@ -339,7 +351,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .Foo object at 0xdeadbeef>.b - failure_demo.py:129: AssertionError + failure_demo.py:128: AssertionError _________________________ test_attribute_instance __________________________ def test_attribute_instance(): @@ -351,7 +363,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 1 = .Foo object at 0xdeadbeef>.b E + where .Foo object at 0xdeadbeef> = .Foo'>() - failure_demo.py:136: AssertionError + failure_demo.py:135: AssertionError __________________________ test_attribute_failure __________________________ def test_attribute_failure(): @@ -364,7 +376,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: i = Foo() > assert i.b == 2 - failure_demo.py:147: + failure_demo.py:146: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = .Foo object at 0xdeadbeef> @@ -373,7 +385,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raise Exception("Failed to get attrib") E Exception: Failed to get attrib - failure_demo.py:142: Exception + failure_demo.py:141: Exception _________________________ test_attribute_multiple __________________________ def test_attribute_multiple(): @@ -390,7 +402,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + and 2 = .Bar object at 0xdeadbeef>.b E + where .Bar object at 0xdeadbeef> = .Bar'>() - failure_demo.py:157: AssertionError + failure_demo.py:156: AssertionError __________________________ TestRaises.test_raises __________________________ self = @@ -400,7 +412,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raises(TypeError, int, s) E ValueError: invalid literal for int() with base 10: 'qwe' - failure_demo.py:167: ValueError + failure_demo.py:166: ValueError ______________________ TestRaises.test_raises_doesnt _______________________ self = @@ -409,7 +421,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raises(OSError, int, "3") E Failed: DID NOT RAISE - failure_demo.py:170: Failed + failure_demo.py:169: Failed __________________________ TestRaises.test_raise ___________________________ self = @@ -418,7 +430,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raise ValueError("demo error") E ValueError: demo error - failure_demo.py:173: ValueError + failure_demo.py:172: ValueError ________________________ TestRaises.test_tupleerror ________________________ self = @@ -427,7 +439,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = [1] # NOQA E ValueError: not enough values to unpack (expected 2, got 1) - failure_demo.py:176: ValueError + failure_demo.py:175: ValueError ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ self = @@ -438,7 +450,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items.pop() E TypeError: cannot unpack non-iterable int object - failure_demo.py:181: TypeError + failure_demo.py:180: TypeError --------------------------- Captured stdout call --------------------------- items is [1, 2, 3] ________________________ TestRaises.test_some_error ________________________ @@ -449,7 +461,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > if namenotexi: # NOQA E NameError: name 'namenotexi' is not defined - failure_demo.py:184: NameError + failure_demo.py:183: NameError ____________________ test_dynamic_compile_shows_nicely _____________________ def test_dynamic_compile_shows_nicely(): @@ -460,19 +472,18 @@ Here is a nice run of several failures and how ``pytest`` presents things: name = "abc-123" spec = importlib.util.spec_from_loader(name, loader=None) module = importlib.util.module_from_spec(spec) - code = _pytest._code.compile(src, name, "exec") + code = compile(src, name, "exec") exec(code, module.__dict__) sys.modules[name] = module > module.foo() - failure_demo.py:203: + failure_demo.py:202: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - def foo(): - > assert 1 == 0 - E AssertionError + > ??? + E AssertionError - <0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:200>:2: AssertionError + abc-123:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ self = @@ -486,9 +497,9 @@ Here is a nice run of several failures and how ``pytest`` presents things: > somefunc(f(), g()) - failure_demo.py:214: + failure_demo.py:213: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - failure_demo.py:11: in somefunc + failure_demo.py:10: in somefunc otherfunc(x, y) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @@ -498,7 +509,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert a == b E assert 44 == 43 - failure_demo.py:7: AssertionError + failure_demo.py:6: AssertionError ___________________ TestMoreErrors.test_z1_unpack_error ____________________ self = @@ -508,7 +519,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E ValueError: not enough values to unpack (expected 2, got 0) - failure_demo.py:218: ValueError + failure_demo.py:217: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ self = @@ -518,7 +529,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E TypeError: cannot unpack non-iterable int object - failure_demo.py:222: TypeError + failure_demo.py:221: TypeError ______________________ TestMoreErrors.test_startswith ______________________ self = @@ -531,7 +542,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = ('456') E + where = '123'.startswith - failure_demo.py:227: AssertionError + failure_demo.py:226: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ self = @@ -550,7 +561,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where '123' = .f at 0xdeadbeef>() E + and '456' = .g at 0xdeadbeef>() - failure_demo.py:236: AssertionError + failure_demo.py:235: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ self = @@ -561,7 +572,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = isinstance(43, float) E + where 43 = globf(42) - failure_demo.py:239: AssertionError + failure_demo.py:238: AssertionError _______________________ TestMoreErrors.test_instance _______________________ self = @@ -572,7 +583,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 42 != 42 E + where 42 = .x - failure_demo.py:243: AssertionError + failure_demo.py:242: AssertionError _______________________ TestMoreErrors.test_compare ________________________ self = @@ -582,7 +593,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 11 < 5 E + where 11 = globf(10) - failure_demo.py:246: AssertionError + failure_demo.py:245: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ self = @@ -593,7 +604,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert x == 0 E assert 1 == 0 - failure_demo.py:251: AssertionError + failure_demo.py:250: AssertionError ___________________ TestCustomAssertMsg.test_single_line ___________________ self = @@ -608,7 +619,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:262: AssertionError + failure_demo.py:261: AssertionError ____________________ TestCustomAssertMsg.test_multiline ____________________ self = @@ -627,7 +638,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:269: AssertionError + failure_demo.py:268: AssertionError ___________________ TestCustomAssertMsg.test_custom_repr ___________________ self = @@ -649,7 +660,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a - failure_demo.py:282: AssertionError + failure_demo.py:281: AssertionError ========================= short test summary info ========================== FAILED failure_demo.py::test_generative[3-6] - assert (3 * 2) < 6 FAILED failure_demo.py::TestFailing::test_simple - assert 42 == 43 diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index d1a1ecdfc9d..e9952dad4d7 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -175,7 +175,7 @@ directory with the above conftest.py: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 0 items @@ -240,7 +240,7 @@ and when running it will see a skipped "slow" test: $ pytest -rs # "-rs" means report details on the little 's' =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 2 items @@ -257,7 +257,7 @@ Or run it including the ``slow`` marked test: $ pytest --runslow =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 2 items @@ -401,7 +401,7 @@ which will add the string to the test header accordingly: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache project deps: mylib-1.1 rootdir: $REGENDOC_TMPDIR @@ -430,7 +430,7 @@ which will add info only when run with "--v": $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache info1: did you know that ... did you? @@ -445,7 +445,7 @@ and nothing when run plainly: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 0 items @@ -485,14 +485,14 @@ Now we can profile which test functions execute the slowest: $ pytest --durations=3 =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 3 items test_some_are_slow.py ... [100%] - ========================= slowest 3 test durations ========================= + =========================== slowest 3 durations ============================ 0.30s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast @@ -591,7 +591,7 @@ If we run this: $ pytest -rx =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 4 items @@ -675,7 +675,7 @@ We can run this: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 7 items @@ -794,7 +794,7 @@ and run them: $ pytest test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 2 items @@ -901,7 +901,7 @@ and run it: $ pytest -s test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 3 items diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index a4e262c2fa3..8d2d063677d 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -151,7 +151,7 @@ marked ``smtp_connection`` fixture function. Running the test looks like this: $ pytest test_smtpsimple.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item @@ -303,7 +303,7 @@ inspect what is going on and can now run the tests: $ pytest test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 2 items @@ -906,10 +906,11 @@ Running the above tests results in the following test IDs being used: $ pytest --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 10 items + @@ -956,7 +957,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``: $ pytest test_fixture_marks.py -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 3 items @@ -1006,7 +1007,7 @@ Here we declare an ``app`` fixture which receives the previously defined $ pytest -v test_appsetup.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 2 items @@ -1086,7 +1087,7 @@ Let's run the tests in verbose mode and with looking at the print-output: $ pytest -v -s test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collecting ... collected 8 items diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index bcfd49ae293..0424faa8825 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.8/site-packages/pytest/__init__.py + pytest 6.0.0rc1 .. _`simpletest`: @@ -53,7 +53,7 @@ That’s it. You can now execute the test function: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item @@ -181,33 +181,30 @@ This is outlined below: .. code-block:: pytest $ pytest -k TestClassDemoInstance -q + FF [100%] + ================================= FAILURES ================================= + ______________________ TestClassDemoInstance.test_one ______________________ - FF [100%] - ================================== FAILURES =================================== - _______________________ TestClassDemoInstance.test_one ________________________ - - self = - request = > + self = - def test_one(self, request): + def test_one(self): > assert 0 E assert 0 - testing\test_example.py:4: AssertionError - _______________________ TestClassDemoInstance.test_two ________________________ + test_class_demo.py:3: AssertionError + ______________________ TestClassDemoInstance.test_two ______________________ - self = - request = > + self = - def test_two(self, request): + def test_two(self): > assert 0 E assert 0 - testing\test_example.py:7: AssertionError - =========================== short test summary info =========================== - FAILED testing/test_example.py::TestClassDemoInstance::test_one - assert 0 - FAILED testing/test_example.py::TestClassDemoInstance::test_two - assert 0 - 2 failed in 0.11s + test_class_demo.py:6: AssertionError + ========================= short test summary info ========================== + FAILED test_class_demo.py::TestClassDemoInstance::test_one - assert 0 + FAILED test_class_demo.py::TestClassDemoInstance::test_two - assert 0 + 2 failed in 0.12s Request a unique temporary directory for functional tests -------------------------------------------------------------- diff --git a/doc/en/index.rst b/doc/en/index.rst index 04c2a36e0c0..f1cb533d787 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -28,7 +28,7 @@ To execute it: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 1e356ebb3ca..c8b0bf4f35a 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -56,7 +56,7 @@ them in turn: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 3 items @@ -123,7 +123,7 @@ Let's run this: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 3 items diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 1a85ecf070a..951a56566d3 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -373,7 +373,7 @@ Running it with the report-on-xfail option gives this output: example $ pytest -rx xfail_demo.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR/example collected 7 items diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index a4f7326fd92..a3749d855a4 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -41,7 +41,7 @@ Running this would result in a passed test except for the last $ pytest test_tmp_path.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item @@ -114,7 +114,7 @@ Running this would result in a passed test except for the last $ pytest test_tmpdir.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index c30b171104e..130e7705b8d 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -137,7 +137,7 @@ the ``self.db`` values in the traceback: $ pytest test_unittest_db.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 2 items diff --git a/doc/en/usage.rst b/doc/en/usage.rst index e25793f8769..aafdeb55f45 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -216,7 +216,7 @@ Example: $ pytest -ra =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 6 items @@ -241,7 +241,7 @@ Example: test_example.py:14: AssertionError ========================= short test summary info ========================== - SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:22: skipping this test + SKIPPED [1] test_example.py:22: skipping this test XFAIL test_example.py::test_xfail reason: xfailing this test XPASS test_example.py::test_xpass always xfail @@ -274,7 +274,7 @@ More than one character can be used, so for example to only see failed and skipp $ pytest -rfs =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 6 items @@ -300,7 +300,7 @@ More than one character can be used, so for example to only see failed and skipp test_example.py:14: AssertionError ========================= short test summary info ========================== FAILED test_example.py::test_fail - assert 0 - SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:22: skipping this test + SKIPPED [1] test_example.py:22: skipping this test == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had @@ -310,7 +310,7 @@ captured output: $ pytest -rpP =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 6 items diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 0b47009d680..6a37b2ad0b9 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -28,7 +28,7 @@ Running pytest now produces this output: $ pytest test_show_warnings.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR collected 1 item diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 27d0fb57c5e..f3e4cbd23ee 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -430,9 +430,9 @@ additionally it is possible to copy examples for an example folder before runnin $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y + platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y cachedir: $PYTHON_PREFIX/.pytest_cache - rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini + rootdir: $REGENDOC_TMPDIR, configfile: pytest.ini collected 2 items test_example.py .. [100%] @@ -443,7 +443,7 @@ additionally it is possible to copy examples for an example folder before runnin testdir.copy_example("test_example.py") test_example.py::test_plugin - $PYTHON_PREFIX/lib/python3.8/site-packages/_pytest/compat.py:333: PytestDeprecationWarning: The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk. + $PYTHON_PREFIX/lib/python3.7/site-packages/_pytest/compat.py:340: PytestDeprecationWarning: The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk. See https://docs.pytest.org/en/stable/deprecations.html#terminalreporter-writer for more information. return getattr(object, name, default) From b22d4663450acd96dfaddd187f6f8c454bcc9fed Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 7 Jul 2020 17:55:41 -0400 Subject: [PATCH 453/823] Remove duplicated users from release announcement --- doc/en/announce/release-6.0.0rc1.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/en/announce/release-6.0.0rc1.rst b/doc/en/announce/release-6.0.0rc1.rst index 32193d5c136..5690b514baf 100644 --- a/doc/en/announce/release-6.0.0rc1.rst +++ b/doc/en/announce/release-6.0.0rc1.rst @@ -30,7 +30,6 @@ Thanks to all who contributed to this release, among them: * Gleb Nikonorov * Hugo van Kemenade * Hunter Richards -* Katarzyna * Katarzyna Król * Katrin Leinweber * Keri Volans @@ -53,7 +52,6 @@ Thanks to all who contributed to this release, among them: * Vlad-Radz * Xinbin Huang * Zac Hatfield-Dodds -* Zac-HD * earonesty * gaurav dhameeja * gdhameeja From c3e2b11a62234c283b4ed4a7bfe5edf7eb6f3d4d Mon Sep 17 00:00:00 2001 From: Arvin Firouzi Date: Thu, 9 Jul 2020 22:10:32 +0200 Subject: [PATCH 454/823] Fix reported location of skip when --runxfail is used (#7432) Co-authored-by: Arvin Firouzi <427014@student.fontys.nl> --- changelog/7392.bugfix.rst | 1 + src/_pytest/skipping.py | 3 ++- testing/test_skipping.py | 25 +++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelog/7392.bugfix.rst diff --git a/changelog/7392.bugfix.rst b/changelog/7392.bugfix.rst new file mode 100644 index 00000000000..48cd949faef --- /dev/null +++ b/changelog/7392.bugfix.rst @@ -0,0 +1 @@ +Fix the reported location of tests skipped with ``@pytest.mark.skip`` when ``--runxfail`` is used. diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index a72bdaabf27..dca2466c49a 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -291,7 +291,8 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): else: rep.outcome = "passed" rep.wasxfail = xfailed.reason - elif ( + + if ( item._store.get(skipped_by_mark_key, True) and rep.skipped and type(rep.longrepr) is tuple diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 0b1c0b49b03..8fceb37aa71 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -235,6 +235,31 @@ def test_func2(): ["*def test_func():*", "*assert 0*", "*1 failed*1 pass*"] ) + @pytest.mark.parametrize( + "test_input,expected", + [ + ( + ["-rs"], + ["SKIPPED [1] test_sample.py:2: unconditional skip", "*1 skipped*"], + ), + ( + ["-rs", "--runxfail"], + ["SKIPPED [1] test_sample.py:2: unconditional skip", "*1 skipped*"], + ), + ], + ) + def test_xfail_run_with_skip_mark(self, testdir, test_input, expected): + testdir.makepyfile( + test_sample=""" + import pytest + @pytest.mark.skip + def test_skip_location() -> None: + assert 0 + """ + ) + result = testdir.runpytest(*test_input) + result.stdout.fnmatch_lines(expected) + def test_xfail_evalfalse_but_fails(self, testdir): item = testdir.getitem( """ From be7b02c3b8ba956f8c201e6a1541bc6e9435b3de Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 9 Jul 2020 22:09:28 -0300 Subject: [PATCH 455/823] Make test_missing_required_plugins xdist-independent Also cleaned up the parametrized list using `pytest.param` to assign ids and removed some redundant cases. Follow up to #7459 --- testing/test_config.py | 171 ++++++++++++++++++++++------------------- 1 file changed, 90 insertions(+), 81 deletions(-) diff --git a/testing/test_config.py b/testing/test_config.py index 3d6590d8e2c..94cb3db5e29 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -6,6 +6,7 @@ from typing import List from typing import Sequence +import attr import py.path import _pytest._code @@ -250,107 +251,115 @@ def pytest_addoption(parser): @pytest.mark.parametrize( "ini_file_text, exception_text", [ - ( - """ - [pytest] - required_plugins = fakePlugin1 fakePlugin2 - """, - "Missing required plugins: fakePlugin1, fakePlugin2", - ), - ( + pytest.param( """ - [pytest] - required_plugins = a pytest-xdist z - """, + [pytest] + required_plugins = a z + """, "Missing required plugins: a, z", + id="2-missing", ), - ( - """ - [pytest] - required_plugins = a q j b c z - """, - "Missing required plugins: a, b, c, j, q, z", - ), - ( - """ - [pytest] - required_plugins = pytest-xdist - """, - "", - ), - ( - """ - [pytest] - required_plugins = pytest-xdist==1.32.0 - """, - "", - ), - ( - """ - [pytest] - required_plugins = pytest-xdist>1.0.0,<2.0.0 - """, - "", - ), - ( + pytest.param( """ - [pytest] - required_plugins = pytest-xdist~=1.32.0 pytest-xdist==1.32.0 pytest-xdist!=0.0.1 pytest-xdist<=99.99.0 - pytest-xdist>=1.32.0 pytest-xdist<9.9.9 pytest-xdist>1.30.0 pytest-xdist===1.32.0 - """, - "", + [pytest] + required_plugins = a z myplugin + """, + "Missing required plugins: a, z", + id="2-missing-1-ok", ), - ( + pytest.param( """ - [pytest] - required_plugins = pytest-xdist>9.9.9 pytest-xdist==1.32.0 pytest-xdist==8.8.8 - """, - "Missing required plugins: pytest-xdist==8.8.8, pytest-xdist>9.9.9", + [pytest] + required_plugins = myplugin + """, + None, + id="1-ok", ), - ( + pytest.param( """ - [pytest] - required_plugins = pytest-xdist==aegsrgrsgs pytest-xdist==-1 pytest-xdist>2.1.1,>3.0.0 - """, - "Missing required plugins: pytest-xdist==-1, pytest-xdist==aegsrgrsgs, pytest-xdist>2.1.1,>3.0.0", + [pytest] + required_plugins = myplugin==1.5 + """, + None, + id="1-ok-pin-exact", ), - ( + pytest.param( """ - [pytest] - required_plugins = pytest-xdist== pytest-xdist<= - """, - "Missing required plugins: pytest-xdist<=, pytest-xdist==", + [pytest] + required_plugins = myplugin>1.0,<2.0 + """, + None, + id="1-ok-pin-loose", ), - ( + pytest.param( """ - [pytest] - required_plugins = pytest-xdist= pytest-xdist< - """, - "Missing required plugins: pytest-xdist<, pytest-xdist=", + [pytest] + required_plugins = pyplugin==1.6 + """, + "Missing required plugins: pyplugin==1.6", + id="missing-version", ), - ( + pytest.param( """ - [some_other_header] - required_plugins = wont be triggered - [pytest] - minversion = 5.0.0 - """, - "", + [pytest] + required_plugins = pyplugin==1.6 other==1.0 + """, + "Missing required plugins: other==1.0, pyplugin==1.6", + id="missing-versions", ), - ( + pytest.param( """ - [pytest] - minversion = 5.0.0 - """, - "", + [some_other_header] + required_plugins = wont be triggered + [pytest] + """, + None, + id="invalid-header", ), ], ) - def test_missing_required_plugins(self, testdir, ini_file_text, exception_text): - pytest.importorskip("xdist") + def test_missing_required_plugins( + self, testdir, monkeypatch, ini_file_text, exception_text + ): + """Check 'required_plugins' option with various settings. - testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) - testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + This test installs a mock "myplugin-1.5" which is used in the parametrized test cases. + """ + + @attr.s + class DummyEntryPoint: + name = attr.ib() + module = attr.ib() + group = "pytest11" + + def load(self): + __import__(self.module) + return sys.modules[self.module] + + entry_points = [ + DummyEntryPoint("myplugin1", "myplugin1_module"), + ] + + @attr.s + class DummyDist: + entry_points = attr.ib() + files = () + version = "1.5" + + @property + def metadata(self): + return {"name": "myplugin"} + + def my_dists(): + return [DummyDist(entry_points)] + + testdir.makepyfile(myplugin1_module="# my plugin module") + testdir.syspathinsert() + + monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) + + testdir.makeini(ini_file_text) if exception_text: with pytest.raises(pytest.fail.Exception, match=exception_text): From fb2640b82fdf6789d36a8204066695c2cc14ff6a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Jun 2020 18:34:32 +0300 Subject: [PATCH 456/823] Stop using ExceptionInfo.errisinstance internally It does the same as a simple isinstance check, but adds a little layer of obscurity on top, which the type checker can't penetrate. --- src/_pytest/_code/code.py | 7 +++++-- src/_pytest/main.py | 2 +- src/_pytest/reports.py | 2 +- src/_pytest/runner.py | 21 ++++++++++++++------- src/_pytest/unittest.py | 4 +--- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index e548bceb76c..ab38b204f17 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -558,7 +558,10 @@ def exconly(self, tryshort: bool = False) -> str: def errisinstance( self, exc: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]] ) -> bool: - """ return True if the exception is an instance of exc """ + """Return True if the exception is an instance of exc. + + Consider using ``isinstance(excinfo.value, exc)`` instead. + """ return isinstance(self.value, exc) def _getreprcrash(self) -> "ReprFileLocation": @@ -804,7 +807,7 @@ def repr_traceback(self, excinfo: ExceptionInfo) -> "ReprTraceback": if self.tbfilter: traceback = traceback.filter() - if excinfo.errisinstance(RecursionError): + if isinstance(excinfo.value, RecursionError): traceback, extraline = self._truncate_recursive_traceback(traceback) else: extraline = None diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 98dabaf87d7..7e68165066d 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -265,7 +265,7 @@ def wrap_session( session.exitstatus = exc.returncode sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc)) else: - if excinfo.errisinstance(SystemExit): + if isinstance(excinfo.value, SystemExit): sys.stderr.write("mainloop: caught unexpected SystemExit!\n") finally: diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 7aba0b0244f..186c53ed321 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -308,7 +308,7 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": if not isinstance(excinfo, ExceptionInfo): outcome = "failed" longrepr = excinfo - elif excinfo.errisinstance(skip.Exception): + elif isinstance(excinfo.value, skip.Exception): outcome = "skipped" r = excinfo._getreprcrash() longrepr = (str(r.path), r.lineno, r.message) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 8b23cb49e55..702380a5b89 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -215,11 +215,18 @@ def call_and_report( def check_interactive_exception(call: "CallInfo", report: BaseReport) -> bool: - return call.excinfo is not None and not ( - hasattr(report, "wasxfail") - or call.excinfo.errisinstance(Skipped) - or call.excinfo.errisinstance(bdb.BdbQuit) - ) + """Check whether the call raised an exception that should be reported as + interactive.""" + if call.excinfo is None: + # Didn't raise. + return False + if hasattr(report, "wasxfail"): + # Exception was expected. + return False + if isinstance(call.excinfo.value, (Skipped, bdb.BdbQuit)): + # Special control flow exception. + return False + return True def call_runtest_hook( @@ -287,7 +294,7 @@ def from_call( result = func() # type: Optional[_T] except BaseException: excinfo = ExceptionInfo.from_current() - if reraise is not None and excinfo.errisinstance(reraise): + if reraise is not None and isinstance(excinfo.value, reraise): raise result = None # use the perf counter @@ -325,7 +332,7 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: if unittest is not None: # Type ignored because unittest is loaded dynamically. skip_exceptions.append(unittest.SkipTest) # type: ignore - if call.excinfo.errisinstance(tuple(skip_exceptions)): + if isinstance(call.excinfo.value, tuple(skip_exceptions)): outcome = "skipped" r_ = collector._repr_failure_py(call.excinfo, "line") assert isinstance(r_, ExceptionChainRepr), repr(r_) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 0e4a3131167..bd61726c7a6 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -302,9 +302,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: if ( unittest and call.excinfo - and call.excinfo.errisinstance( - unittest.SkipTest # type: ignore[attr-defined] # noqa: F821 - ) + and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined] ): excinfo = call.excinfo # let's substitute the excinfo with a pytest.skip one From e079ebbd57534e1d0041e22f091054e4cd6f2ecb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Jun 2020 19:31:35 +0300 Subject: [PATCH 457/823] python: more type annotations --- src/_pytest/python.py | 106 +++++++++++++++++++------------------ testing/python/metafunc.py | 18 ++++--- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index b41a234f4b8..2c9f383e410 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -13,7 +13,9 @@ from functools import partial from typing import Callable from typing import Dict +from typing import Generator from typing import Iterable +from typing import Iterator from typing import List from typing import Mapping from typing import Optional @@ -196,8 +198,8 @@ def pytest_collect_file(path: py.path.local, parent) -> Optional["Module"]: return None -def path_matches_patterns(path, patterns): - """Returns True if the given py.path.local matches one of the patterns in the list of globs given""" +def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool: + """Returns True if path matches any of the patterns in the list of globs given.""" return any(path.fnmatch(pattern) for pattern in patterns) @@ -300,7 +302,7 @@ def _getobj(self): obj = self.parent.obj # type: ignore[attr-defined] # noqa: F821 return getattr(obj, self.name) - def getmodpath(self, stopatmodule=True, includemodule=False): + def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str: """ return python path relative to the containing module. """ chain = self.listchain() chain.reverse() @@ -338,10 +340,10 @@ def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]: class PyCollector(PyobjMixin, nodes.Collector): - def funcnamefilter(self, name): + def funcnamefilter(self, name: str) -> bool: return self._matches_prefix_or_glob_option("python_functions", name) - def isnosetest(self, obj): + def isnosetest(self, obj: object) -> bool: """ Look for the __test__ attribute, which is applied by the @nose.tools.istest decorator """ @@ -350,10 +352,10 @@ def isnosetest(self, obj): # function) as test classes. return safe_getattr(obj, "__test__", False) is True - def classnamefilter(self, name): + def classnamefilter(self, name: str) -> bool: return self._matches_prefix_or_glob_option("python_classes", name) - def istestfunction(self, obj, name): + def istestfunction(self, obj: object, name: str) -> bool: if self.funcnamefilter(name) or self.isnosetest(obj): if isinstance(obj, staticmethod): # static methods need to be unwrapped @@ -365,10 +367,10 @@ def istestfunction(self, obj, name): else: return False - def istestclass(self, obj, name): + def istestclass(self, obj: object, name: str) -> bool: return self.classnamefilter(name) or self.isnosetest(obj) - def _matches_prefix_or_glob_option(self, option_name, name): + def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool: """ checks if the given name matches the prefix or glob-pattern defined in ini configuration. @@ -428,7 +430,7 @@ def _makeitem( ) # type: Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]] return item - def _genfunctions(self, name, funcobj): + def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]: modulecol = self.getparent(Module) assert modulecol is not None module = modulecol.obj @@ -486,7 +488,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: self.session._fixturemanager.parsefactories(self) return super().collect() - def _inject_setup_module_fixture(self): + def _inject_setup_module_fixture(self) -> None: """Injects a hidden autouse, module scoped fixture into the collected module object that invokes setUpModule/tearDownModule if either or both are available. @@ -504,7 +506,7 @@ def _inject_setup_module_fixture(self): return @fixtures.fixture(autouse=True, scope="module") - def xunit_setup_module_fixture(request): + def xunit_setup_module_fixture(request) -> Generator[None, None, None]: if setup_module is not None: _call_with_optional_argument(setup_module, request.module) yield @@ -513,7 +515,7 @@ def xunit_setup_module_fixture(request): self.obj.__pytest_setup_module = xunit_setup_module_fixture - def _inject_setup_function_fixture(self): + def _inject_setup_function_fixture(self) -> None: """Injects a hidden autouse, function scoped fixture into the collected module object that invokes setup_function/teardown_function if either or both are available. @@ -528,7 +530,7 @@ def _inject_setup_function_fixture(self): return @fixtures.fixture(autouse=True, scope="function") - def xunit_setup_function_fixture(request): + def xunit_setup_function_fixture(request) -> Generator[None, None, None]: if request.instance is not None: # in this case we are bound to an instance, so we need to let # setup_method handle this @@ -608,7 +610,7 @@ def __init__( ) self.name = os.path.basename(str(fspath.dirname)) - def setup(self): + def setup(self) -> None: # not using fixtures to call setup_module here because autouse fixtures # from packages are not called automatically (#4085) setup_module = _get_first_non_fixture_func( @@ -661,7 +663,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: pkg_prefixes.add(path) -def _call_with_optional_argument(func, arg): +def _call_with_optional_argument(func, arg) -> None: """Call the given function with the given argument if func accepts one argument, otherwise calls func without arguments""" arg_count = func.__code__.co_argcount @@ -673,7 +675,7 @@ def _call_with_optional_argument(func, arg): func() -def _get_first_non_fixture_func(obj, names): +def _get_first_non_fixture_func(obj: object, names: Iterable[str]): """Return the attribute from the given object to be used as a setup/teardown xunit-style function, but only if not marked as a fixture to avoid calling it twice. @@ -723,7 +725,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: return [Instance.from_parent(self, name="()")] - def _inject_setup_class_fixture(self): + def _inject_setup_class_fixture(self) -> None: """Injects a hidden autouse, class scoped fixture into the collected class object that invokes setup_class/teardown_class if either or both are available. @@ -736,7 +738,7 @@ def _inject_setup_class_fixture(self): return @fixtures.fixture(autouse=True, scope="class") - def xunit_setup_class_fixture(cls): + def xunit_setup_class_fixture(cls) -> Generator[None, None, None]: if setup_class is not None: func = getimfunc(setup_class) _call_with_optional_argument(func, self.obj) @@ -747,7 +749,7 @@ def xunit_setup_class_fixture(cls): self.obj.__pytest_setup_class = xunit_setup_class_fixture - def _inject_setup_method_fixture(self): + def _inject_setup_method_fixture(self) -> None: """Injects a hidden autouse, function scoped fixture into the collected class object that invokes setup_method/teardown_method if either or both are available. @@ -760,7 +762,7 @@ def _inject_setup_method_fixture(self): return @fixtures.fixture(autouse=True, scope="function") - def xunit_setup_method_fixture(self, request): + def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]: method = request.function if setup_method is not None: func = getattr(self, "setup_method") @@ -794,16 +796,18 @@ def newinstance(self): return self.obj -def hasinit(obj): - init = getattr(obj, "__init__", None) +def hasinit(obj: object) -> bool: + init = getattr(obj, "__init__", None) # type: object if init: return init != object.__init__ + return False -def hasnew(obj): - new = getattr(obj, "__new__", None) +def hasnew(obj: object) -> bool: + new = getattr(obj, "__new__", None) # type: object if new: return new != object.__new__ + return False class CallSpec2: @@ -843,7 +847,7 @@ def id(self) -> str: def setmulti2( self, - valtypes: "Mapping[str, Literal['params', 'funcargs']]", + valtypes: Mapping[str, "Literal['params', 'funcargs']"], argnames: typing.Sequence[str], valset: Iterable[object], id: str, @@ -903,7 +907,7 @@ def __init__( self._arg2fixturedefs = fixtureinfo.name2fixturedefs @property - def funcargnames(self): + def funcargnames(self) -> List[str]: """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" warnings.warn(FUNCARGNAMES, stacklevel=2) return self.fixturenames @@ -1170,7 +1174,11 @@ def _validate_if_using_arg_names( ) -def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): +def _find_parametrized_scope( + argnames: typing.Sequence[str], + arg2fixturedefs: Mapping[str, typing.Sequence[fixtures.FixtureDef]], + indirect: Union[bool, typing.Sequence[str]], +) -> "fixtures._Scope": """Find the most appropriate scope for a parametrized call based on its arguments. When there's at least one direct argument, always use "function" scope. @@ -1180,9 +1188,7 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): Related to issue #1832, based on code posted by @Kingdread. """ - from _pytest.fixtures import scopes - - if isinstance(indirect, (list, tuple)): + if isinstance(indirect, Sequence): all_arguments_are_fixtures = len(indirect) == len(argnames) else: all_arguments_are_fixtures = bool(indirect) @@ -1196,7 +1202,7 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): ] if used_scopes: # Takes the most narrow scope from used fixtures - for scope in reversed(scopes): + for scope in reversed(fixtures.scopes): if scope in used_scopes: return scope @@ -1264,7 +1270,7 @@ def _idvalset( ids: Optional[List[Union[None, str]]], nodeid: Optional[str], config: Optional[Config], -): +) -> str: if parameterset.id is not None: return parameterset.id id = None if ids is None or idx >= len(ids) else ids[idx] @@ -1318,7 +1324,7 @@ def show_fixtures_per_test(config): return wrap_session(config, _show_fixtures_per_test) -def _show_fixtures_per_test(config, session): +def _show_fixtures_per_test(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() @@ -1330,7 +1336,7 @@ def get_best_relpath(func): loc = getlocation(func, curdir) return curdir.bestrelpath(py.path.local(loc)) - def write_fixture(fixture_def): + def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: argname = fixture_def.argname if verbose <= 0 and argname.startswith("_"): return @@ -1346,18 +1352,16 @@ def write_fixture(fixture_def): else: tw.line(" no docstring available", red=True) - def write_item(item): - try: - info = item._fixtureinfo - except AttributeError: - # doctests items have no _fixtureinfo attribute - return - if not info.name2fixturedefs: - # this test item does not use any fixtures + def write_item(item: nodes.Item) -> None: + # Not all items have _fixtureinfo attribute. + info = getattr(item, "_fixtureinfo", None) # type: Optional[FuncFixtureInfo] + if info is None or not info.name2fixturedefs: + # This test item does not use any fixtures. return tw.line() tw.sep("-", "fixtures used by {}".format(item.name)) - tw.sep("-", "({})".format(get_best_relpath(item.function))) + # TODO: Fix this type ignore. + tw.sep("-", "({})".format(get_best_relpath(item.function))) # type: ignore[attr-defined] # dict key not used in loop but needed for sorting for _, fixturedefs in sorted(info.name2fixturedefs.items()): assert fixturedefs is not None @@ -1448,15 +1452,15 @@ class Function(PyobjMixin, nodes.Item): def __init__( self, - name, + name: str, parent, - config=None, + config: Optional[Config] = None, callspec: Optional[CallSpec2] = None, callobj=NOTSET, keywords=None, - session=None, + session: Optional[Session] = None, fixtureinfo: Optional[FuncFixtureInfo] = None, - originalname=None, + originalname: Optional[str] = None, ) -> None: """ param name: the full function name, including any decorations like those @@ -1533,8 +1537,8 @@ def from_parent(cls, parent, **kw): # todo: determine sound type limitations """ return super().from_parent(parent=parent, **kw) - def _initrequest(self): - self.funcargs = {} + def _initrequest(self) -> None: + self.funcargs = {} # type: Dict[str, object] self._request = fixtures.FixtureRequest(self) @property @@ -1552,7 +1556,7 @@ def _pyfuncitem(self): return self @property - def funcargnames(self): + def funcargnames(self) -> List[str]: """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" warnings.warn(FUNCARGNAMES, stacklevel=2) return self.fixturenames diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 1f110d41954..6a841a34692 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -3,9 +3,12 @@ import sys import textwrap from typing import Any +from typing import cast +from typing import Dict from typing import Iterator from typing import List from typing import Optional +from typing import Sequence from typing import Tuple from typing import Union @@ -138,12 +141,15 @@ def test_find_parametrized_scope(self) -> None: class DummyFixtureDef: scope = attr.ib() - fixtures_defs = dict( - session_fix=[DummyFixtureDef("session")], - package_fix=[DummyFixtureDef("package")], - module_fix=[DummyFixtureDef("module")], - class_fix=[DummyFixtureDef("class")], - func_fix=[DummyFixtureDef("function")], + fixtures_defs = cast( + Dict[str, Sequence[fixtures.FixtureDef]], + dict( + session_fix=[DummyFixtureDef("session")], + package_fix=[DummyFixtureDef("package")], + module_fix=[DummyFixtureDef("module")], + class_fix=[DummyFixtureDef("class")], + func_fix=[DummyFixtureDef("function")], + ), ) # use arguments to determine narrow scope; the cause of the bug is that it would look on all From 5da4a1d84f1d5c49ce9d6e8c63ce7df7f7a92b24 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 9 Jul 2020 23:23:30 +0300 Subject: [PATCH 458/823] capture: type annotate return value of fixtures --- src/_pytest/capture.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index daded6395ee..3f9c60fb9a0 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -821,7 +821,7 @@ def disabled(self) -> Generator[None, None, None]: @pytest.fixture -def capsys(request: SubRequest): +def capsys(request: SubRequest) -> Generator[CaptureFixture, None, None]: """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method @@ -838,7 +838,7 @@ def capsys(request: SubRequest): @pytest.fixture -def capsysbinary(request: SubRequest): +def capsysbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]: """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` @@ -855,7 +855,7 @@ def capsysbinary(request: SubRequest): @pytest.fixture -def capfd(request: SubRequest): +def capfd(request: SubRequest) -> Generator[CaptureFixture, None, None]: """Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -872,7 +872,7 @@ def capfd(request: SubRequest): @pytest.fixture -def capfdbinary(request: SubRequest): +def capfdbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]: """Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method From c7a1db5d01596e38154d7b29f14df130c49505cb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 9 Jul 2020 23:30:04 +0300 Subject: [PATCH 459/823] junitxml: few typing fixes & additions --- src/_pytest/junitxml.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 71a31b85b76..4873e722dd8 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -14,6 +14,7 @@ import re import sys from datetime import datetime +from typing import Callable from typing import Dict from typing import List from typing import Match @@ -70,7 +71,7 @@ class Junit(py.xml.Namespace): _py_ext_re = re.compile(r"\.py$") -def bin_xml_escape(arg: str) -> py.xml.raw: +def bin_xml_escape(arg: object) -> py.xml.raw: def repl(matchobj: Match[str]) -> str: i = ord(matchobj.group()) if i <= 0xFF: @@ -78,7 +79,7 @@ def repl(matchobj: Match[str]) -> str: else: return "#x%04X" % i - return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg))) + return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(str(arg)))) def merge_family(left, right) -> None: @@ -118,10 +119,10 @@ def append(self, node: py.xml.Tag) -> None: self.xml.add_stats(type(node).__name__) self.nodes.append(node) - def add_property(self, name: str, value: str) -> None: + def add_property(self, name: str, value: object) -> None: self.properties.append((str(name), bin_xml_escape(value))) - def add_attribute(self, name: str, value: str) -> None: + def add_attribute(self, name: str, value: object) -> None: self.attrs[str(name)] = bin_xml_escape(value) def make_properties_node(self) -> Union[py.xml.Tag, str]: @@ -301,12 +302,14 @@ def _warn_incompatibility_with_xunit2( @pytest.fixture -def record_property(request: FixtureRequest): - """Add an extra properties the calling test. +def record_property(request: FixtureRequest) -> Callable[[str, object], None]: + """Add extra properties to the calling test. + User properties become part of the test report and are available to the configured reporters, like JUnit XML. - The fixture is callable with ``(name, value)``, with value being automatically - xml-encoded. + + The fixture is callable with ``name, value``. The value is automatically + XML-encoded. Example:: @@ -322,10 +325,11 @@ def append_property(name: str, value: object) -> None: @pytest.fixture -def record_xml_attribute(request: FixtureRequest): +def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], None]: """Add extra xml attributes to the tag for the calling test. - The fixture is callable with ``(name, value)``, with value being - automatically xml-encoded + + The fixture is callable with ``name, value``. The value is + automatically XML-encoded. """ from _pytest.warning_types import PytestExperimentalApiWarning @@ -336,7 +340,7 @@ def record_xml_attribute(request: FixtureRequest): _warn_incompatibility_with_xunit2(request, "record_xml_attribute") # Declare noop - def add_attr_noop(name: str, value: str) -> None: + def add_attr_noop(name: str, value: object) -> None: pass attr_func = add_attr_noop @@ -359,7 +363,7 @@ def _check_record_param_type(param: str, v: str) -> None: @pytest.fixture(scope="session") -def record_testsuite_property(request: FixtureRequest): +def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]: """ Records a new ```` tag as child of the root ````. This is suitable to writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family. @@ -377,7 +381,7 @@ def test_foo(record_testsuite_property): __tracebackhide__ = True - def record_func(name: str, value: str): + def record_func(name: str, value: object) -> None: """noop function in case --junitxml was not passed in the command-line""" __tracebackhide__ = True _check_record_param_type("name", name) @@ -693,7 +697,7 @@ def pytest_sessionfinish(self) -> None: def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None: terminalreporter.write_sep("-", "generated xml file: {}".format(self.logfile)) - def add_global_property(self, name: str, value: str) -> None: + def add_global_property(self, name: str, value: object) -> None: __tracebackhide__ = True _check_record_param_type("name", name) self.global_properties.append((name, bin_xml_escape(value))) From bcff02c4c608ed6123e23bbada08cb404abd0354 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 9 Jul 2020 23:50:15 +0300 Subject: [PATCH 460/823] pytester: some type annotations --- src/_pytest/pytester.py | 57 ++++++++++++++++++++------------------- testing/python/collect.py | 6 ++++- testing/test_nodes.py | 2 +- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 45f6f008ab1..594abee9094 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -32,6 +32,7 @@ from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.config import PytestPluginManager from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureRequest from _pytest.main import Session @@ -210,7 +211,7 @@ class HookRecorder: """ - def __init__(self, pluginmanager) -> None: + def __init__(self, pluginmanager: PytestPluginManager) -> None: self._pluginmanager = pluginmanager self.calls = [] # type: List[ParsedCall] @@ -376,7 +377,7 @@ def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]": @pytest.fixture -def testdir(request: FixtureRequest, tmpdir_factory) -> "Testdir": +def testdir(request: FixtureRequest, tmpdir_factory: TempdirFactory) -> "Testdir": """ A :class: `TestDir` instance, that can be used to run and test pytest itself. @@ -388,7 +389,7 @@ def testdir(request: FixtureRequest, tmpdir_factory) -> "Testdir": @pytest.fixture -def _sys_snapshot(): +def _sys_snapshot() -> Generator[None, None, None]: snappaths = SysPathsSnapshot() snapmods = SysModulesSnapshot() yield @@ -526,7 +527,7 @@ def restore(self) -> None: class SysModulesSnapshot: - def __init__(self, preserve: Optional[Callable[[str], bool]] = None): + def __init__(self, preserve: Optional[Callable[[str], bool]] = None) -> None: self.__preserve = preserve self.__saved = dict(sys.modules) @@ -605,13 +606,13 @@ def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> N # Do not use colors for inner runs by default. mp.setenv("PY_COLORS", "0") - def __repr__(self): + def __repr__(self) -> str: return "".format(self.tmpdir) - def __str__(self): + def __str__(self) -> str: return str(self.tmpdir) - def finalize(self): + def finalize(self) -> None: """Clean up global state artifacts. Some methods modify the global interpreter state and this tries to @@ -624,7 +625,7 @@ def finalize(self): self._cwd_snapshot.restore() self.monkeypatch.undo() - def __take_sys_modules_snapshot(self): + def __take_sys_modules_snapshot(self) -> SysModulesSnapshot: # some zope modules used by twisted-related tests keep internal state # and can't be deleted; we had some trouble in the past with # `zope.interface` for example @@ -633,13 +634,13 @@ def preserve_module(name): return SysModulesSnapshot(preserve=preserve_module) - def make_hook_recorder(self, pluginmanager): + def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder: """Create a new :py:class:`HookRecorder` for a PluginManager.""" pluginmanager.reprec = reprec = HookRecorder(pluginmanager) self.request.addfinalizer(reprec.finish_recording) return reprec - def chdir(self): + def chdir(self) -> None: """Cd into the temporary directory. This is done automatically upon instantiation. @@ -647,7 +648,7 @@ def chdir(self): """ self.tmpdir.chdir() - def _makefile(self, ext, lines, files, encoding="utf-8"): + def _makefile(self, ext: str, lines, files, encoding: str = "utf-8"): items = list(files.items()) def to_text(s): @@ -669,7 +670,7 @@ def to_text(s): ret = p return ret - def makefile(self, ext, *args, **kwargs): + def makefile(self, ext: str, *args: str, **kwargs): r"""Create new file(s) in the testdir. :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`. @@ -698,7 +699,7 @@ def makeini(self, source): """Write a tox.ini file with 'source' as contents.""" return self.makefile(".ini", tox=source) - def getinicfg(self, source): + def getinicfg(self, source) -> IniConfig: """Return the pytest section from the tox.ini config file.""" p = self.makeini(source) return IniConfig(p)["pytest"] @@ -748,7 +749,7 @@ def test_something(testdir): """ return self._makefile(".txt", args, kwargs) - def syspathinsert(self, path=None): + def syspathinsert(self, path=None) -> None: """Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`. This is undone automatically when this object dies at the end of each @@ -759,11 +760,11 @@ def syspathinsert(self, path=None): self.monkeypatch.syspath_prepend(str(path)) - def mkdir(self, name): + def mkdir(self, name) -> py.path.local: """Create a new (sub)directory.""" return self.tmpdir.mkdir(name) - def mkpydir(self, name): + def mkpydir(self, name) -> py.path.local: """Create a new python package. This creates a (sub)directory with an empty ``__init__.py`` file so it @@ -774,7 +775,7 @@ def mkpydir(self, name): p.ensure("__init__.py") return p - def copy_example(self, name=None): + def copy_example(self, name=None) -> py.path.local: """Copy file from project's directory into the testdir. :param str name: The name of the file to copy. @@ -826,7 +827,7 @@ def copy_example(self, name=None): Session = Session - def getnode(self, config, arg): + def getnode(self, config: Config, arg): """Return the collection node of a file. :param config: :py:class:`_pytest.config.Config` instance, see @@ -861,7 +862,7 @@ def getpathnode(self, path): config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) return res - def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]: + def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]: """Generate all test items from a collection node. This recurses into the collection node and returns a list of all the @@ -974,7 +975,7 @@ def pytest_configure(x, config: Config) -> None: class reprec: # type: ignore pass - reprec.ret = ret + reprec.ret = ret # type: ignore[attr-defined] # typically we reraise keyboard interrupts from the child run # because it's our user requesting interruption of the testing @@ -1083,7 +1084,7 @@ def parseconfigure(self, *args) -> Config: config._do_configure() return config - def getitem(self, source, funcname="test_func"): + def getitem(self, source, funcname: str = "test_func") -> Item: """Return the test item for a test function. This writes the source to a python file and runs pytest's collection on @@ -1104,7 +1105,7 @@ def getitem(self, source, funcname="test_func"): funcname, source, items ) - def getitems(self, source): + def getitems(self, source) -> List[Item]: """Return all test items collected from the module. This writes the source to a python file and runs pytest's collection on @@ -1114,7 +1115,7 @@ def getitems(self, source): modcol = self.getmodulecol(source) return self.genitems([modcol]) - def getmodulecol(self, source, configargs=(), withinit=False): + def getmodulecol(self, source, configargs=(), withinit: bool = False): """Return the module collection node for ``source``. This writes ``source`` to a file using :py:meth:`makepyfile` and then @@ -1199,7 +1200,9 @@ def popen( return popen - def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: + def run( + self, *cmdargs, timeout: Optional[float] = None, stdin=CLOSE_STDIN + ) -> RunResult: """Run a command with arguments. Run a process using subprocess.Popen saving the stdout and stderr. @@ -1238,7 +1241,7 @@ def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: if isinstance(stdin, bytes): popen.stdin.close() - def handle_timeout(): + def handle_timeout() -> None: __tracebackhide__ = True timeout_message = ( @@ -1283,7 +1286,7 @@ def _dump_lines(self, lines, fp): except UnicodeEncodeError: print("couldn't print to {} because of encoding".format(fp)) - def _getpytestargs(self): + def _getpytestargs(self) -> Tuple[str, ...]: return sys.executable, "-mpytest" def runpython(self, script) -> RunResult: @@ -1298,7 +1301,7 @@ def runpython_c(self, command): """Run python -c "command", return a :py:class:`RunResult`.""" return self.run(sys.executable, "-c", command) - def runpytest_subprocess(self, *args, timeout=None) -> RunResult: + def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunResult: """Run pytest as a subprocess with given arguments. Any plugins added to the :py:attr:`plugins` list will be added using the diff --git a/testing/python/collect.py b/testing/python/collect.py index e98a21f1cc5..691380a0b74 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -675,7 +675,11 @@ def test_no_param(): pass """ ) - assert [x.originalname for x in items] == [ + originalnames = [] + for x in items: + assert isinstance(x, pytest.Function) + originalnames.append(x.originalname) + assert originalnames == [ "test_func", "test_func", "test_no_param", diff --git a/testing/test_nodes.py b/testing/test_nodes.py index e5d8ffd713b..cc6e562a5a4 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -38,7 +38,7 @@ def test(): """ ) with pytest.raises(ValueError, match=".*instance of PytestWarning.*"): - items[0].warn(UserWarning("some warning")) + items[0].warn(UserWarning("some warning")) # type: ignore[arg-type] def test__check_initialpaths_for_relpath() -> None: From 8e8d63927691f0abd814be252b6e9bbb4adec7c3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Jul 2020 00:27:47 +0300 Subject: [PATCH 461/823] tmpdir: type annotations --- src/_pytest/tmpdir.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index f6d1799ad6f..58dd659087d 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -46,7 +46,7 @@ def from_config(cls, config) -> "TempPathFactory": given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir") ) - def _ensure_relative_to_basetemp(self, basename: str): + def _ensure_relative_to_basetemp(self, basename: str) -> str: basename = os.path.normpath(basename) if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp(): raise ValueError( @@ -119,7 +119,7 @@ def mktemp(self, basename: str, numbered: bool = True) -> py.path.local: """ return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve()) - def getbasetemp(self): + def getbasetemp(self) -> py.path.local: """backward compat wrapper for ``_tmppath_factory.getbasetemp``""" return py.path.local(self._tmppath_factory.getbasetemp().resolve()) @@ -176,7 +176,7 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: @pytest.fixture -def tmpdir(tmp_path): +def tmpdir(tmp_path: Path) -> py.path.local: """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary From 168d9adefc21de98e91e06199a27cac205765c06 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Jul 2020 00:41:43 +0300 Subject: [PATCH 462/823] hookspec: change Node -> Union[Item, Collector] to avoid exposing Node We don't really want `Node` itself as a public API, only its two subclasses. --- src/_pytest/hookspec.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 8c88b66cb17..cf3da400a8f 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -32,7 +32,6 @@ from _pytest.main import Session from _pytest.nodes import Collector from _pytest.nodes import Item - from _pytest.nodes import Node from _pytest.outcomes import Exit from _pytest.python import Function from _pytest.python import Metafunc @@ -827,7 +826,7 @@ def pytest_keyboard_interrupt( def pytest_exception_interact( - node: "Node", + node: Union["Item", "Collector"], call: "CallInfo[object]", report: Union["CollectReport", "TestReport"], ) -> None: From fc702ab7e41a583b0b0658aa2bf7c962a19d3fe7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Jul 2020 00:50:10 +0300 Subject: [PATCH 463/823] fixtures: some type annotations --- src/_pytest/fixtures.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index cedcd462559..52dc78b6305 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -809,7 +809,9 @@ def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int: class FixtureLookupError(LookupError): """ could not return a requested Fixture (missing or invalid). """ - def __init__(self, argname, request, msg: Optional[str] = None) -> None: + def __init__( + self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = None + ) -> None: self.argname = argname self.request = request self.fixturestack = request._get_fixturestack() @@ -861,7 +863,14 @@ def formatrepr(self) -> "FixtureLookupErrorRepr": class FixtureLookupErrorRepr(TerminalRepr): - def __init__(self, filename, firstlineno, tblines, errorstring, argname): + def __init__( + self, + filename: Union[str, py.path.local], + firstlineno: int, + tblines: Sequence[str], + errorstring: str, + argname: Optional[str], + ) -> None: self.tblines = tblines self.errorstring = errorstring self.filename = filename From a2f021b6f3618f30d0cd5604797ce5069353d864 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Jul 2020 09:44:14 +0300 Subject: [PATCH 464/823] Remove no longer needed `noqa: F821` uses Not needed since pyflakes 2.2.0. --- src/_pytest/_io/saferepr.py | 4 ++-- src/_pytest/debugging.py | 4 ++-- src/_pytest/doctest.py | 10 ++++----- src/_pytest/fixtures.py | 22 +++++++++---------- src/_pytest/junitxml.py | 2 +- src/_pytest/logging.py | 4 ++-- src/_pytest/mark/structures.py | 4 ++-- src/_pytest/nodes.py | 4 ++-- src/_pytest/python.py | 6 ++--- src/_pytest/recwarn.py | 2 +- src/_pytest/runner.py | 12 +++++----- src/_pytest/setuponly.py | 4 ++-- src/_pytest/skipping.py | 2 +- src/_pytest/terminal.py | 2 +- src/_pytest/unittest.py | 12 +++++----- testing/code/test_excinfo.py | 2 +- .../test_compare_two_different_dataclasses.py | 2 +- testing/io/test_saferepr.py | 8 +++---- testing/python/collect.py | 6 ++--- testing/python/fixtures.py | 10 ++++----- testing/python/metafunc.py | 10 ++++----- testing/python/raises.py | 12 +++++----- testing/test_assertrewrite.py | 10 ++++----- testing/test_capture.py | 2 +- testing/test_collection.py | 6 ++--- testing/test_config.py | 2 +- testing/test_doctest.py | 2 +- testing/test_junitxml.py | 2 +- testing/test_mark.py | 8 +++---- testing/test_nodes.py | 4 ++-- testing/test_pytester.py | 14 ++++++------ testing/test_runner.py | 4 ++-- testing/test_store.py | 2 +- testing/test_terminal.py | 2 +- 34 files changed, 101 insertions(+), 101 deletions(-) diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 6b9f353a227..823b8d71942 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -98,12 +98,12 @@ def _format( level: int, ) -> None: # Type ignored because _dispatch is private. - p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined] # noqa: F821 + p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined] objid = id(object) if objid in context or p is None: # Type ignored because _format is private. - super()._format( # type: ignore[misc] # noqa: F821 + super()._format( # type: ignore[misc] object, stream, indent, allowance, context, level, ) return diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 63126cbe02f..3677d3bf915 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -146,7 +146,7 @@ def _get_pdb_wrapper_class(cls, pdb_cls, capman: "CaptureManager"): # Type ignored because mypy doesn't support "dynamic" # inheritance like this. - class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc] # noqa: F821 + class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc] _pytest_capman = capman _continued = False @@ -349,7 +349,7 @@ def _enter_pdb( rep.toterminal(tw) tw.sep(">", "entering PDB") tb = _postmortem_traceback(excinfo) - rep._pdbshown = True # type: ignore[attr-defined] # noqa: F821 + rep._pdbshown = True # type: ignore[attr-defined] post_mortem(tb) return rep diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 181c66b95ff..ebf0d584cc3 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -284,7 +284,7 @@ def runtest(self) -> None: failures = [] # type: List[doctest.DocTestFailure] # Type ignored because we change the type of `out` from what # doctest expects. - self.runner.run(self.dtest, out=failures) # type: ignore[arg-type] # noqa: F821 + self.runner.run(self.dtest, out=failures) # type: ignore[arg-type] if failures: raise MultipleDoctestFailures(failures) @@ -302,7 +302,7 @@ def _disable_output_capturing_for_darwin(self) -> None: sys.stderr.write(err) # TODO: Type ignored -- breaks Liskov Substitution. - def repr_failure( # type: ignore[override] # noqa: F821 + def repr_failure( # type: ignore[override] self, excinfo: ExceptionInfo[BaseException], ) -> Union[str, TerminalRepr]: import doctest @@ -329,7 +329,7 @@ def repr_failure( # type: ignore[override] # noqa: F821 lineno = test.lineno + example.lineno + 1 message = type(failure).__name__ # TODO: ReprFileLocation doesn't expect a None lineno. - reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] # noqa: F821 + reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] checker = _get_checker() report_choice = _get_report_choice( self.config.getoption("doctestreport") @@ -567,9 +567,9 @@ def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest: def func() -> None: pass - doctest_item.funcargs = {} # type: ignore[attr-defined] # noqa: F821 + doctest_item.funcargs = {} # type: ignore[attr-defined] fm = doctest_item.session._fixturemanager - doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined] # noqa: F821 + doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined] node=doctest_item, func=func, cls=None, funcargs=False ) fixture_request = FixtureRequest(doctest_item) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 52dc78b6305..aef28a1cad0 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -248,7 +248,7 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator the specified scope. """ assert scopenum < scopenum_function # function try: - callspec = item.callspec # type: ignore[attr-defined] # noqa: F821 + callspec = item.callspec # type: ignore[attr-defined] except AttributeError: pass else: @@ -266,7 +266,7 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator elif scopenum == 2: # module key = (argname, param_index, item.fspath) elif scopenum == 3: # class - item_cls = item.cls # type: ignore[attr-defined] # noqa: F821 + item_cls = item.cls # type: ignore[attr-defined] key = (argname, param_index, item.fspath, item_cls) yield key @@ -477,7 +477,7 @@ def _getnextfixturedef(self, argname: str) -> "FixtureDef": fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) # TODO: Fix this type ignore. Either add assert or adjust types. # Can this be None here? - self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment] # noqa: F821 + self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment] # fixturedefs list is immutable so we maintain a decreasing index index = self._arg2index.get(argname, 0) - 1 if fixturedefs is None or (-index > len(fixturedefs)): @@ -723,7 +723,7 @@ def _getscopeitem(self, scope): if scope == "package": # FIXME: _fixturedef is not defined on FixtureRequest (this class), # but on FixtureRequest (a subclass). - node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined] # noqa: F821 + node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined] else: node = get_scope_node(self._pyfuncitem, scope) if node is None and scope == "class": @@ -944,7 +944,7 @@ def _eval_scope_callable( try: # Type ignored because there is no typing mechanism to specify # keyword arguments, currently. - result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg] # noqa: F821 + result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg] except Exception as e: raise TypeError( "Error evaluating {} while defining fixture '{}'.\n" @@ -1081,7 +1081,7 @@ def resolve_fixture_function( if fixturedef.unittest: if request.instance is not None: # bind the unbound method to the TestCase instance - fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr] # noqa: F821 + fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr] else: # the fixture function needs to be bound to the actual # request.instance so that code working with "fixturedef" behaves @@ -1090,12 +1090,12 @@ def resolve_fixture_function( # handle the case where fixture is defined not in a test class, but some other class # (for example a plugin class with a fixture), see #2270 if hasattr(fixturefunc, "__self__") and not isinstance( - request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr] # noqa: F821 + request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr] ): return fixturefunc fixturefunc = getimfunc(fixturedef.func) if fixturefunc != fixturedef.func: - fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr] # noqa: F821 + fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr] return fixturefunc @@ -1167,7 +1167,7 @@ def result(*args, **kwargs): # keep reference to the original function in our own custom attribute so we don't unwrap # further than this point and lose useful wrappings like @mock.patch (#3774) - result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined] # noqa: F821 + result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined] return result @@ -1209,7 +1209,7 @@ def __call__(self, function: _FixtureFunction) -> _FixtureFunction: ) # Type ignored because https://github.com/python/mypy/issues/2087. - function._pytestfixturefunction = self # type: ignore[attr-defined] # noqa: F821 + function._pytestfixturefunction = self # type: ignore[attr-defined] return function @@ -1503,7 +1503,7 @@ def getfixtureinfo( def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: nodeid = None try: - p = py.path.local(plugin.__file__) # type: ignore[attr-defined] # noqa: F821 + p = py.path.local(plugin.__file__) # type: ignore[attr-defined] except AttributeError: pass else: diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 4873e722dd8..8c68d196a2c 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -281,7 +281,7 @@ def finalize(self) -> None: self.__dict__.clear() # Type ignored becuase mypy doesn't like overriding a method. # Also the return value doesn't match... - self.to_xml = lambda: py.xml.raw(data) # type: ignore # noqa: F821 + self.to_xml = lambda: py.xml.raw(data) # type: ignore def _warn_incompatibility_with_xunit2( diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 52d75e66d9f..1df0643a6df 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -165,7 +165,7 @@ def format(self, record: logging.LogRecord) -> str: if "\n" in record.message: if hasattr(record, "auto_indent"): # passed in from the "extra={}" kwarg on the call to logging.log() - auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined] # noqa: F821 + auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined] else: auto_indent = self._auto_indent @@ -755,7 +755,7 @@ def __init__( :param _pytest.terminal.TerminalReporter terminal_reporter: :param _pytest.capture.CaptureManager capture_manager: """ - logging.StreamHandler.__init__(self, stream=terminal_reporter) # type: ignore[arg-type] # noqa: F821 + logging.StreamHandler.__init__(self, stream=terminal_reporter) # type: ignore[arg-type] self.capture_manager = capture_manager self.reset() self.set_when(None) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 0c9344f3fc0..38369388cc5 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -124,7 +124,7 @@ def extract_from( # # @pytest.mark.parametrize(('x', 'y'), [1, 2]) # def test_foo(x, y): pass - return cls(parameterset, marks=[], id=None) # type: ignore[arg-type] # noqa: F821 + return cls(parameterset, marks=[], id=None) # type: ignore[arg-type] @staticmethod def _parse_parametrize_args( @@ -320,7 +320,7 @@ def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": # return type. Not much we can do about that. Thankfully mypy picks # the first match so it works out even if we break the rules. @overload - def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc] # noqa: F821 + def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc] raise NotImplementedError() @overload # noqa: F811 diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 24e4665865b..560548aea64 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -457,7 +457,7 @@ def collect(self) -> Iterable[Union["Item", "Collector"]]: raise NotImplementedError("abstract") # TODO: This omits the style= parameter which breaks Liskov Substitution. - def repr_failure( # type: ignore[override] # noqa: F821 + def repr_failure( # type: ignore[override] self, excinfo: ExceptionInfo[BaseException] ) -> Union[str, TerminalRepr]: """ @@ -600,7 +600,7 @@ def _collectfile( else: duplicate_paths.add(path) - return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] # noqa: F723 + return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] class File(FSCollector): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2c9f383e410..7209bf1edab 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -299,7 +299,7 @@ def _getobj(self): """Gets the underlying Python object. May be overwritten by subclasses.""" # TODO: Improve the type of `parent` such that assert/ignore aren't needed. assert self.parent is not None - obj = self.parent.obj # type: ignore[attr-defined] # noqa: F821 + obj = self.parent.obj # type: ignore[attr-defined] return getattr(obj, self.name) def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str: @@ -784,7 +784,7 @@ class Instance(PyCollector): def _getobj(self): # TODO: Improve the type of `parent` such that assert/ignore aren't needed. assert self.parent is not None - obj = self.parent.obj # type: ignore[attr-defined] # noqa: F821 + obj = self.parent.obj # type: ignore[attr-defined] return obj() def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: @@ -1593,7 +1593,7 @@ def _prunetraceback(self, excinfo: ExceptionInfo) -> None: entry.set_repr_style("short") # TODO: Type ignored -- breaks Liskov Substitution. - def repr_failure( # type: ignore[override] # noqa: F821 + def repr_failure( # type: ignore[override] self, excinfo: ExceptionInfo[BaseException], ) -> Union[str, TerminalRepr]: style = self.config.getoption("tbstyle", "auto") diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 49bb909ccfc..11ca571aadd 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -165,7 +165,7 @@ class WarningsRecorder(warnings.catch_warnings): def __init__(self) -> None: # Type ignored due to the way typeshed handles warnings.catch_warnings. - super().__init__(record=True) # type: ignore[call-arg] # noqa: F821 + super().__init__(record=True) # type: ignore[call-arg] self._entered = False self._list = [] # type: List[warnings.WarningMessage] diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 702380a5b89..69754ad5e10 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -106,8 +106,8 @@ def runtestprotocol( item: Item, log: bool = True, nextitem: Optional[Item] = None ) -> List[TestReport]: hasrequest = hasattr(item, "_request") - if hasrequest and not item._request: # type: ignore[attr-defined] # noqa: F821 - item._initrequest() # type: ignore[attr-defined] # noqa: F821 + if hasrequest and not item._request: # type: ignore[attr-defined] + item._initrequest() # type: ignore[attr-defined] rep = call_and_report(item, "setup", log) reports = [rep] if rep.passed: @@ -119,8 +119,8 @@ def runtestprotocol( # after all teardown hooks have been called # want funcargs and request info to go away if hasrequest: - item._request = False # type: ignore[attr-defined] # noqa: F821 - item.funcargs = None # type: ignore[attr-defined] # noqa: F821 + item._request = False # type: ignore[attr-defined] + item.funcargs = None # type: ignore[attr-defined] return reports @@ -422,7 +422,7 @@ def prepare(self, colitem) -> None: # check if the last collection node has raised an error for col in self.stack: if hasattr(col, "_prepare_exc"): - exc = col._prepare_exc # type: ignore[attr-defined] # noqa: F821 + exc = col._prepare_exc # type: ignore[attr-defined] raise exc needed_collectors = colitem.listchain() @@ -431,7 +431,7 @@ def prepare(self, colitem) -> None: try: col.setup() except TEST_OUTCOME as e: - col._prepare_exc = e # type: ignore[attr-defined] # noqa: F821 + col._prepare_exc = e # type: ignore[attr-defined] raise e diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 932d0c279b7..dfd01cc76b2 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -43,7 +43,7 @@ def pytest_fixture_setup( param = fixturedef.ids[request.param_index] else: param = request.param - fixturedef.cached_param = param # type: ignore[attr-defined] # noqa: F821 + fixturedef.cached_param = param # type: ignore[attr-defined] _show_fixture_action(fixturedef, "SETUP") @@ -53,7 +53,7 @@ def pytest_fixture_post_finalizer(fixturedef: FixtureDef) -> None: if config.option.setupshow: _show_fixture_action(fixturedef, "TEARDOWN") if hasattr(fixturedef, "cached_param"): - del fixturedef.cached_param # type: ignore[attr-defined] # noqa: F821 + del fixturedef.cached_param # type: ignore[attr-defined] def _show_fixture_action(fixturedef: FixtureDef, msg: str) -> None: diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index dca2466c49a..335e10996a2 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -56,7 +56,7 @@ def pytest_configure(config: Config) -> None: def nop(*args, **kwargs): pass - nop.Exception = xfail.Exception # type: ignore[attr-defined] # noqa: F821 + nop.Exception = xfail.Exception # type: ignore[attr-defined] setattr(pytest, "xfail", nop) config.addinivalue_line( diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 6c19e56dda8..ef9da50f3f8 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1219,7 +1219,7 @@ def _get_line_with_reprcrash_message( try: # Type ignored intentionally -- possible AttributeError expected. - msg = rep.longrepr.reprcrash.message # type: ignore[union-attr] # noqa: F821 + msg = rep.longrepr.reprcrash.message # type: ignore[union-attr] except AttributeError: pass else: diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index bd61726c7a6..782a5c36962 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -102,13 +102,13 @@ def _inject_setup_teardown_fixtures(self, cls: type) -> None: cls, "setUpClass", "tearDownClass", scope="class", pass_self=False ) if class_fixture: - cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined] # noqa: F821 + cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined] method_fixture = _make_xunit_fixture( cls, "setup_method", "teardown_method", scope="function", pass_self=True ) if method_fixture: - cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined] # noqa: F821 + cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined] def _make_xunit_fixture( @@ -148,7 +148,7 @@ def setup(self) -> None: # a bound method to be called during teardown() if set (see 'runtest()') self._explicit_tearDown = None # type: Optional[Callable[[], None]] assert self.parent is not None - self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined] # noqa: F821 + self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined] self._obj = getattr(self._testcase, self.name) if hasattr(self, "_request"): self._request._fillfixtures() @@ -167,7 +167,7 @@ def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None: # unwrap potential exception info (see twisted trial support below) rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) try: - excinfo = _pytest._code.ExceptionInfo(rawexcinfo) # type: ignore[arg-type] # noqa: F821 + excinfo = _pytest._code.ExceptionInfo(rawexcinfo) # type: ignore[arg-type] # invoke the attributes to trigger storing the traceback # trial causes some issue there excinfo.value @@ -259,7 +259,7 @@ def runtest(self) -> None: # let the unittest framework handle async functions if is_async_function(self.obj): # Type ignored because self acts as the TestResult, but is not actually one. - self._testcase(result=self) # type: ignore[arg-type] # noqa: F821 + self._testcase(result=self) # type: ignore[arg-type] else: # when --pdb is given, we want to postpone calling tearDown() otherwise # when entering the pdb prompt, tearDown() would have probably cleaned up @@ -275,7 +275,7 @@ def runtest(self) -> None: # wrap_pytest_function_for_tracing replaces self.obj by a wrapper setattr(self._testcase, self.name, self.obj) try: - self._testcase(result=self) # type: ignore[arg-type] # noqa: F821 + self._testcase(result=self) # type: ignore[arg-type] finally: delattr(self._testcase, self.name) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 6ee848e54ae..060f52cc7a0 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -752,7 +752,7 @@ def entry(): from _pytest._code.code import Code monkeypatch.setattr(Code, "path", "bogus") - excinfo.traceback[0].frame.code.path = "bogus" # type: ignore[misc] # noqa: F821 + excinfo.traceback[0].frame.code.path = "bogus" # type: ignore[misc] p = FormattedExcinfo(style="short") reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) lines = reprtb.lines diff --git a/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py index 22e981e33f5..0a4820c69ba 100644 --- a/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py +++ b/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py @@ -16,4 +16,4 @@ class SimpleDataObjectTwo: left = SimpleDataObjectOne(1, "b") right = SimpleDataObjectTwo(1, "c") - assert left != right # type: ignore[comparison-overlap] # noqa: F821 + assert left != right # type: ignore[comparison-overlap] diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 6912a113fa3..7a97cf424c5 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -34,8 +34,8 @@ def __repr__(self): raise self.ex class BrokenReprException(Exception): - __str__ = None # type: ignore[assignment] # noqa: F821 - __repr__ = None # type: ignore[assignment] # noqa: F821 + __str__ = None # type: ignore[assignment] + __repr__ = None # type: ignore[assignment] assert "Exception" in saferepr(BrokenRepr(Exception("broken"))) s = saferepr(BrokenReprException("really broken")) @@ -44,7 +44,7 @@ class BrokenReprException(Exception): none = None try: - none() # type: ignore[misc] # noqa: F821 + none() # type: ignore[misc] except BaseException as exc: exp_exc = repr(exc) obj = BrokenRepr(BrokenReprException("omg even worse")) @@ -139,7 +139,7 @@ def test_big_repr(): def test_repr_on_newstyle() -> None: class Function: def __repr__(self): - return "<%s>" % (self.name) # type: ignore[attr-defined] # noqa: F821 + return "<%s>" % (self.name) # type: ignore[attr-defined] assert saferepr(Function()) diff --git a/testing/python/collect.py b/testing/python/collect.py index 691380a0b74..80c962d7091 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -724,10 +724,10 @@ def test_fail(): assert 0 assert fn1 != fn3 for fn in fn1, fn2, fn3: - assert fn != 3 # type: ignore[comparison-overlap] # noqa: F821 + assert fn != 3 # type: ignore[comparison-overlap] assert fn != modcol - assert fn != [1, 2, 3] # type: ignore[comparison-overlap] # noqa: F821 - assert [1, 2, 3] != fn # type: ignore[comparison-overlap] # noqa: F821 + assert fn != [1, 2, 3] # type: ignore[comparison-overlap] + assert [1, 2, 3] != fn # type: ignore[comparison-overlap] assert modcol != fn def test_allow_sane_sorting_for_decorators(self, testdir): diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 3efbbe10757..ca3408ece30 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3850,7 +3850,7 @@ def test_foo(f1, p1, m1, f2, s1): pass ) testdir.runpytest() # actual fixture execution differs: dependent fixtures must be created first ("my_tmpdir") - FIXTURE_ORDER = pytest.FIXTURE_ORDER # type: ignore[attr-defined] # noqa: F821 + FIXTURE_ORDER = pytest.FIXTURE_ORDER # type: ignore[attr-defined] assert FIXTURE_ORDER == "s1 my_tmpdir_factory p1 m1 my_tmpdir f1 f2".split() def test_func_closure_module(self, testdir): @@ -4159,7 +4159,7 @@ def test_fixture_duplicated_arguments() -> None: """Raise error if there are positional and keyword arguments for the same parameter (#1682).""" with pytest.raises(TypeError) as excinfo: - @pytest.fixture("session", scope="session") # type: ignore[call-overload] # noqa: F821 + @pytest.fixture("session", scope="session") # type: ignore[call-overload] def arg(arg): pass @@ -4171,7 +4171,7 @@ def arg(arg): with pytest.raises(TypeError) as excinfo: - @pytest.fixture( # type: ignore[call-overload] # noqa: F821 + @pytest.fixture( # type: ignore[call-overload] "function", ["p1"], True, @@ -4199,7 +4199,7 @@ def test_fixture_with_positionals() -> None: with pytest.warns(pytest.PytestDeprecationWarning) as warnings: - @pytest.fixture("function", [0], True) # type: ignore[call-overload] # noqa: F821 + @pytest.fixture("function", [0], True) # type: ignore[call-overload] def fixture_with_positionals(): pass @@ -4213,7 +4213,7 @@ def fixture_with_positionals(): def test_fixture_with_too_many_positionals() -> None: with pytest.raises(TypeError) as excinfo: - @pytest.fixture("function", [0], True, ["id"], "name", "extra") # type: ignore[call-overload] # noqa: F821 + @pytest.fixture("function", [0], True, ["id"], "name", "extra") # type: ignore[call-overload] def fixture_with_positionals(): pass diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 6a841a34692..4e6cfaf91ba 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -77,7 +77,7 @@ def func(x, y): pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5, 6])) with pytest.raises(TypeError, match="^ids must be a callable or an iterable$"): - metafunc.parametrize("y", [5, 6], ids=42) # type: ignore[arg-type] # noqa: F821 + metafunc.parametrize("y", [5, 6], ids=42) # type: ignore[arg-type] def test_parametrize_error_iterator(self) -> None: def func(x): @@ -95,7 +95,7 @@ def gen() -> Iterator[Union[int, None, Exc]]: metafunc = self.Metafunc(func) # When the input is an iterator, only len(args) are taken, # so the bad Exc isn't reached. - metafunc.parametrize("x", [1, 2], ids=gen()) # type: ignore[arg-type] # noqa: F821 + metafunc.parametrize("x", [1, 2], ids=gen()) # type: ignore[arg-type] assert [(x.funcargs, x.id) for x in metafunc._calls] == [ ({"x": 1}, "0"), ({"x": 2}, "2"), @@ -107,7 +107,7 @@ def gen() -> Iterator[Union[int, None, Exc]]: r" Exc\(from_gen\) \(type: \) at index 2" ), ): - metafunc.parametrize("x", [1, 2, 3], ids=gen()) # type: ignore[arg-type] # noqa: F821 + metafunc.parametrize("x", [1, 2, 3], ids=gen()) # type: ignore[arg-type] def test_parametrize_bad_scope(self) -> None: def func(x): @@ -118,7 +118,7 @@ def func(x): fail.Exception, match=r"parametrize\(\) call in func got an unexpected scope value 'doggy'", ): - metafunc.parametrize("x", [1], scope="doggy") # type: ignore[arg-type] # noqa: F821 + metafunc.parametrize("x", [1], scope="doggy") # type: ignore[arg-type] def test_parametrize_request_name(self, testdir: Testdir) -> None: """Show proper error when 'request' is used as a parameter name in parametrize (#6183)""" @@ -675,7 +675,7 @@ def func(x, y): fail.Exception, match="In func: expected Sequence or boolean for indirect, got dict", ): - metafunc.parametrize("x, y", [("a", "b")], indirect={}) # type: ignore[arg-type] # noqa: F821 + metafunc.parametrize("x, y", [("a", "b")], indirect={}) # type: ignore[arg-type] def test_parametrize_indirect_list_functional(self, testdir: Testdir) -> None: """ diff --git a/testing/python/raises.py b/testing/python/raises.py index e55eb6f5472..3f378d015a6 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -8,7 +8,7 @@ class TestRaises: def test_check_callable(self) -> None: with pytest.raises(TypeError, match=r".* must be callable"): - pytest.raises(RuntimeError, "int('qwe')") # type: ignore[call-overload] # noqa: F821 + pytest.raises(RuntimeError, "int('qwe')") # type: ignore[call-overload] def test_raises(self): excinfo = pytest.raises(ValueError, int, "qwe") @@ -30,7 +30,7 @@ def __call__(self): def test_raises_falsey_type_error(self) -> None: with pytest.raises(TypeError): - with pytest.raises(AssertionError, match=0): # type: ignore[call-overload] # noqa: F821 + with pytest.raises(AssertionError, match=0): # type: ignore[call-overload] raise AssertionError("ohai") def test_raises_repr_inflight(self): @@ -128,11 +128,11 @@ def test_division(example_input, expectation): def test_noclass(self) -> None: with pytest.raises(TypeError): - pytest.raises("wrong", lambda: None) # type: ignore[call-overload] # noqa: F821 + pytest.raises("wrong", lambda: None) # type: ignore[call-overload] def test_invalid_arguments_to_raises(self) -> None: with pytest.raises(TypeError, match="unknown"): - with pytest.raises(TypeError, unknown="bogus"): # type: ignore[call-overload] # noqa: F821 + with pytest.raises(TypeError, unknown="bogus"): # type: ignore[call-overload] raise ValueError() def test_tuple(self): @@ -262,12 +262,12 @@ def __class__(self): assert False, "via __class__" with pytest.raises(AssertionError) as excinfo: - with pytest.raises(CrappyClass()): # type: ignore[call-overload] # noqa: F821 + with pytest.raises(CrappyClass()): # type: ignore[call-overload] pass assert "via __class__" in excinfo.value.args[0] def test_raises_context_manager_with_kwargs(self): with pytest.raises(TypeError) as excinfo: - with pytest.raises(Exception, foo="bar"): # type: ignore[call-overload] # noqa: F821 + with pytest.raises(Exception, foo="bar"): # type: ignore[call-overload] pass assert "Unexpected keyword arguments" in str(excinfo.value) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 38893deb9a2..e403bb2ec9b 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -51,7 +51,7 @@ def getmsg( exec(code, ns) func = ns[f.__name__] try: - func() # type: ignore[operator] # noqa: F821 + func() # type: ignore[operator] except AssertionError: if must_pass: pytest.fail("shouldn't have raised") @@ -174,7 +174,7 @@ def f3() -> None: assert getmsg(f3, {"a_global": False}) == "assert False" def f4() -> None: - assert sys == 42 # type: ignore[comparison-overlap] # noqa: F821 + assert sys == 42 # type: ignore[comparison-overlap] verbose = request.config.getoption("verbose") msg = getmsg(f4, {"sys": sys}) @@ -188,7 +188,7 @@ def f4() -> None: assert msg == "assert sys == 42" def f5() -> None: - assert cls == 42 # type: ignore[name-defined] # noqa: F821 + assert cls == 42 # type: ignore[name-defined] # noqa: F821 class X: pass @@ -684,7 +684,7 @@ def myany(x) -> bool: def test_formatchar(self) -> None: def f() -> None: - assert "%test" == "test" # type: ignore[comparison-overlap] # noqa: F821 + assert "%test" == "test" # type: ignore[comparison-overlap] msg = getmsg(f) assert msg is not None @@ -1264,7 +1264,7 @@ def spy_find_spec(name, path): # use default patterns, otherwise we inherit pytest's testing config hook.fnpats[:] = ["test_*.py", "*_test.py"] monkeypatch.setattr(hook, "_find_spec", spy_find_spec) - hook.set_session(StubSession()) # type: ignore[arg-type] # noqa: F821 + hook.set_session(StubSession()) # type: ignore[arg-type] testdir.syspathinsert() return hook diff --git a/testing/test_capture.py b/testing/test_capture.py index 9e5036a6682..a3bd4b62327 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1537,7 +1537,7 @@ def test_encodedfile_writelines(tmpfile: BinaryIO) -> None: ef = capture.EncodedFile(tmpfile, encoding="utf-8") with pytest.raises(TypeError): ef.writelines([b"line1", b"line2"]) - assert ef.writelines(["line3", "line4"]) is None # type: ignore[func-returns-value] # noqa: F821 + assert ef.writelines(["line3", "line4"]) is None # type: ignore[func-returns-value] ef.flush() tmpfile.seek(0) assert tmpfile.read() == b"line3line4" diff --git a/testing/test_collection.py b/testing/test_collection.py index d7a9b0439aa..6bab509d03f 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -41,10 +41,10 @@ def test_fail(): assert 0 for fn in fn1, fn2, fn3: assert isinstance(fn, pytest.Function) - assert fn != 3 # type: ignore[comparison-overlap] # noqa: F821 + assert fn != 3 # type: ignore[comparison-overlap] assert fn != modcol - assert fn != [1, 2, 3] # type: ignore[comparison-overlap] # noqa: F821 - assert [1, 2, 3] != fn # type: ignore[comparison-overlap] # noqa: F821 + assert fn != [1, 2, 3] # type: ignore[comparison-overlap] + assert [1, 2, 3] != fn # type: ignore[comparison-overlap] assert modcol != fn assert testdir.collect_by_name(modcol, "doesnotexist") is None diff --git a/testing/test_config.py b/testing/test_config.py index 94cb3db5e29..9b1c11d5edc 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1602,7 +1602,7 @@ class DummyPlugin: # args cannot be None with pytest.raises(TypeError): - Config.InvocationParams(args=None, plugins=None, dir=Path()) # type: ignore[arg-type] # noqa: F821 + Config.InvocationParams(args=None, plugins=None, dir=Path()) # type: ignore[arg-type] @pytest.mark.parametrize( diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 9ef9417cd7e..965dba6c179 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1490,7 +1490,7 @@ def test_warning_on_unwrap_of_broken_object( pytest.PytestWarning, match="^Got KeyError.* when unwrapping" ): with pytest.raises(KeyError): - inspect.unwrap(bad_instance, stop=stop) # type: ignore[arg-type] # noqa: F821 + inspect.unwrap(bad_instance, stop=stop) # type: ignore[arg-type] assert inspect.unwrap.__module__ == "inspect" diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 5e5826b236a..eb8475ca55c 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1124,7 +1124,7 @@ class Report(BaseReport): node_reporter.append_skipped(test_report) test_report.longrepr = "filename", 1, "Skipped: 卡嘣嘣" node_reporter.append_skipped(test_report) - test_report.wasxfail = ustr # type: ignore[attr-defined] # noqa: F821 + test_report.wasxfail = ustr # type: ignore[attr-defined] node_reporter.append_skipped(test_report) log.pytest_sessionfinish() diff --git a/testing/test_mark.py b/testing/test_mark.py index f261c8922ad..f35660093e7 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -20,7 +20,7 @@ def test_pytest_exists_in_namespace_all(self, attr: str, modulename: str) -> Non def test_pytest_mark_notcallable(self) -> None: mark = Mark() with pytest.raises(TypeError): - mark() # type: ignore[operator] # noqa: F821 + mark() # type: ignore[operator] def test_mark_with_param(self): def some_function(abc): @@ -31,10 +31,10 @@ class SomeClass: assert pytest.mark.foo(some_function) is some_function marked_with_args = pytest.mark.foo.with_args(some_function) - assert marked_with_args is not some_function # type: ignore[comparison-overlap] # noqa: F821 + assert marked_with_args is not some_function # type: ignore[comparison-overlap] assert pytest.mark.foo(SomeClass) is SomeClass - assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap] # noqa: F821 + assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap] def test_pytest_mark_name_starts_with_underscore(self): mark = Mark() @@ -1077,7 +1077,7 @@ def test_custom_mark_parametrized(obj_type): def test_pytest_param_id_requires_string() -> None: with pytest.raises(TypeError) as excinfo: - pytest.param(id=True) # type: ignore[arg-type] # noqa: F821 + pytest.param(id=True) # type: ignore[arg-type] (msg,) = excinfo.value.args assert msg == "Expected id to be a string, got : True" diff --git a/testing/test_nodes.py b/testing/test_nodes.py index cc6e562a5a4..f9026ec619f 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -25,9 +25,9 @@ def test_ischildnode(baseid: str, nodeid: str, expected: bool) -> None: def test_node_from_parent_disallowed_arguments() -> None: with pytest.raises(TypeError, match="session is"): - nodes.Node.from_parent(None, session=None) # type: ignore[arg-type] # noqa: F821 + nodes.Node.from_parent(None, session=None) # type: ignore[arg-type] with pytest.raises(TypeError, match="config is"): - nodes.Node.from_parent(None, config=None) # type: ignore[arg-type] # noqa: F821 + nodes.Node.from_parent(None, config=None) # type: ignore[arg-type] def test_std_warn_not_pytestwarning(testdir: Testdir) -> None: diff --git a/testing/test_pytester.py b/testing/test_pytester.py index d0afb40b07d..46f3e1cabfa 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -484,20 +484,20 @@ def test_linematcher_with_nonlist() -> None: lm = LineMatcher([]) with pytest.raises(TypeError, match="invalid type for lines2: set"): - lm.fnmatch_lines(set()) # type: ignore[arg-type] # noqa: F821 + lm.fnmatch_lines(set()) # type: ignore[arg-type] with pytest.raises(TypeError, match="invalid type for lines2: dict"): - lm.fnmatch_lines({}) # type: ignore[arg-type] # noqa: F821 + lm.fnmatch_lines({}) # type: ignore[arg-type] with pytest.raises(TypeError, match="invalid type for lines2: set"): - lm.re_match_lines(set()) # type: ignore[arg-type] # noqa: F821 + lm.re_match_lines(set()) # type: ignore[arg-type] with pytest.raises(TypeError, match="invalid type for lines2: dict"): - lm.re_match_lines({}) # type: ignore[arg-type] # noqa: F821 + lm.re_match_lines({}) # type: ignore[arg-type] with pytest.raises(TypeError, match="invalid type for lines2: Source"): - lm.fnmatch_lines(Source()) # type: ignore[arg-type] # noqa: F821 + lm.fnmatch_lines(Source()) # type: ignore[arg-type] lm.fnmatch_lines([]) lm.fnmatch_lines(()) lm.fnmatch_lines("") - assert lm._getlines({}) == {} # type: ignore[arg-type,comparison-overlap] # noqa: F821 - assert lm._getlines(set()) == set() # type: ignore[arg-type,comparison-overlap] # noqa: F821 + assert lm._getlines({}) == {} # type: ignore[arg-type,comparison-overlap] + assert lm._getlines(set()) == set() # type: ignore[arg-type,comparison-overlap] assert lm._getlines(Source()) == [] assert lm._getlines(Source("pass\npass")) == ["pass", "pass"] diff --git a/testing/test_runner.py b/testing/test_runner.py index 474ff4df817..def3f910d52 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -884,7 +884,7 @@ def runtest(self): raise IndexError("TEST") try: - runner.pytest_runtest_call(ItemMightRaise()) # type: ignore[arg-type] # noqa: F821 + runner.pytest_runtest_call(ItemMightRaise()) # type: ignore[arg-type] except IndexError: pass # Check that exception info is stored on sys @@ -895,7 +895,7 @@ def runtest(self): # The next run should clear the exception info stored by the previous run ItemMightRaise.raise_error = False - runner.pytest_runtest_call(ItemMightRaise()) # type: ignore[arg-type] # noqa: F821 + runner.pytest_runtest_call(ItemMightRaise()) # type: ignore[arg-type] assert not hasattr(sys, "last_type") assert not hasattr(sys, "last_value") assert not hasattr(sys, "last_traceback") diff --git a/testing/test_store.py b/testing/test_store.py index 98014887ec1..b6d4208a092 100644 --- a/testing/test_store.py +++ b/testing/test_store.py @@ -47,7 +47,7 @@ def test_store() -> None: # Can't accidentally add attributes to store object itself. with pytest.raises(AttributeError): - store.foo = "nope" # type: ignore[attr-defined] # noqa: F821 + store.foo = "nope" # type: ignore[attr-defined] # No interaction with anoter store. store2 = Store() diff --git a/testing/test_terminal.py b/testing/test_terminal.py index f1481dce5e4..19aff99545c 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1699,7 +1699,7 @@ def test_summary_stats( class fake_session: testscollected = 0 - tr._session = fake_session # type: ignore[assignment] # noqa: F821 + tr._session = fake_session # type: ignore[assignment] assert tr._is_last_item # Reset cache. From 087b047426415ed46bdd5eb2d5c7c8f00294468b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Jul 2020 10:08:16 +0300 Subject: [PATCH 465/823] cacheprovider: type annotations --- src/_pytest/cacheprovider.py | 10 +++++----- src/_pytest/pathlib.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 00f62b60c77..de7ee914980 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -80,7 +80,7 @@ def clear_cache(cls, cachedir: Path) -> None: rm_rf(d) @staticmethod - def cache_dir_from_config(config: Config): + def cache_dir_from_config(config: Config) -> Path: return resolve_from_str(config.getini("cache_dir"), config.rootdir) def warn(self, fmt: str, **args: object) -> None: @@ -113,7 +113,7 @@ def makedir(self, name: str) -> py.path.local: def _getvaluepath(self, key: str) -> Path: return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key)) - def get(self, key, default): + def get(self, key: str, default): """ return cached value for the given key. If no value was yet cached or the value cannot be read, the specified default is returned. @@ -131,7 +131,7 @@ def get(self, key, default): except (ValueError, OSError): return default - def set(self, key, value) -> None: + def set(self, key: str, value: object) -> None: """ save value for the given key. :param key: must be a ``/`` separated value. Usually the first @@ -522,7 +522,7 @@ def cacheshow(config: Config, session: Session) -> int: vdir = basedir / Cache._CACHE_PREFIX_VALUES tw.sep("-", "cache values for %r" % glob) for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()): - key = valpath.relative_to(vdir) + key = str(valpath.relative_to(vdir)) val = config.cache.get(key, dummy) if val is dummy: tw.line("%s contains unreadable content, will be ignored" % key) @@ -539,6 +539,6 @@ def cacheshow(config: Config, session: Session) -> int: # if p.check(dir=1): # print("%s/" % p.relto(basedir)) if p.is_file(): - key = p.relative_to(basedir) + key = str(p.relative_to(basedir)) tw.line("{} is a file of length {:d}".format(key, p.stat().st_size)) return 0 diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index dd7443f07e3..6a0bf7f6f0e 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -363,15 +363,15 @@ def make_numbered_dir_with_cleanup( raise e -def resolve_from_str(input: str, root): +def resolve_from_str(input: str, root: py.path.local) -> Path: assert not isinstance(input, Path), "would break on py2" - root = Path(root) + rootpath = Path(root) input = expanduser(input) input = expandvars(input) if isabs(input): return Path(input) else: - return root.joinpath(input) + return rootpath.joinpath(input) def fnmatch_ex(pattern: str, path) -> bool: From 77f3cb4baade9cf0a02bd779dde9b11766bda0d4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Jul 2020 11:50:36 +0300 Subject: [PATCH 466/823] code/code: type annotations & doc cleanups --- src/_pytest/_code/code.py | 180 ++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 95 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index ab38b204f17..aa28fea187d 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -51,7 +51,7 @@ class Code: - """ wrapper around Python code objects """ + """Wrapper around Python code objects.""" def __init__(self, rawcode) -> None: if not hasattr(rawcode, "co_filename"): @@ -74,7 +74,7 @@ def __ne__(self, other): @property def path(self) -> Union[py.path.local, str]: - """ return a path object pointing to source code (or a str in case + """Return a path object pointing to source code (or a str in case of OSError / non-existing file). """ if not self.raw.co_filename: @@ -92,24 +92,22 @@ def path(self) -> Union[py.path.local, str]: @property def fullsource(self) -> Optional["Source"]: - """ return a _pytest._code.Source object for the full source file of the code - """ + """Return a _pytest._code.Source object for the full source file of the code.""" full, _ = findsource(self.raw) return full def source(self) -> "Source": - """ return a _pytest._code.Source object for the code object's source only - """ + """Return a _pytest._code.Source object for the code object's source only.""" # return source only for that part of code return Source(self.raw) def getargs(self, var: bool = False) -> Tuple[str, ...]: - """ return a tuple with the argument names for the code object + """Return a tuple with the argument names for the code object. - if 'var' is set True also return the names of the variable and - keyword arguments when present + If 'var' is set True also return the names of the variable and + keyword arguments when present. """ - # handfull shortcut for getting args + # Handy shortcut for getting args. raw = self.raw argcount = raw.co_argcount if var: @@ -131,44 +129,43 @@ def __init__(self, frame: FrameType) -> None: @property def statement(self) -> "Source": - """ statement this frame is at """ + """Statement this frame is at.""" if self.code.fullsource is None: return Source("") return self.code.fullsource.getstatement(self.lineno) def eval(self, code, **vars): - """ evaluate 'code' in the frame + """Evaluate 'code' in the frame. - 'vars' are optional additional local variables + 'vars' are optional additional local variables. - returns the result of the evaluation + Returns the result of the evaluation. """ f_locals = self.f_locals.copy() f_locals.update(vars) return eval(code, self.f_globals, f_locals) def exec_(self, code, **vars) -> None: - """ exec 'code' in the frame + """Exec 'code' in the frame. - 'vars' are optional; additional local variables + 'vars' are optional; additional local variables. """ f_locals = self.f_locals.copy() f_locals.update(vars) exec(code, self.f_globals, f_locals) def repr(self, object: object) -> str: - """ return a 'safe' (non-recursive, one-line) string repr for 'object' - """ + """Return a 'safe' (non-recursive, one-line) string repr for 'object'.""" return saferepr(object) def is_true(self, object): return object def getargs(self, var: bool = False): - """ return a list of tuples (name, value) for all arguments + """Return a list of tuples (name, value) for all arguments. - if 'var' is set True also include the variable and keyword - arguments when present + If 'var' is set True, also include the variable and keyword arguments + when present. """ retval = [] for arg in self.code.getargs(var): @@ -180,12 +177,16 @@ def getargs(self, var: bool = False): class TracebackEntry: - """ a single entry in a traceback """ + """A single entry in a Traceback.""" _repr_style = None # type: Optional[Literal["short", "long"]] exprinfo = None - def __init__(self, rawentry: TracebackType, excinfo=None) -> None: + def __init__( + self, + rawentry: TracebackType, + excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, + ) -> None: self._excinfo = excinfo self._rawentry = rawentry self.lineno = rawentry.tb_lineno - 1 @@ -207,26 +208,26 @@ def __repr__(self) -> str: @property def statement(self) -> "Source": - """ _pytest._code.Source object for the current statement """ + """_pytest._code.Source object for the current statement.""" source = self.frame.code.fullsource assert source is not None return source.getstatement(self.lineno) @property def path(self) -> Union[py.path.local, str]: - """ path to the source code """ + """Path to the source code.""" return self.frame.code.path @property def locals(self) -> Dict[str, Any]: - """ locals of underlying frame """ + """Locals of underlying frame.""" return self.frame.f_locals def getfirstlinesource(self) -> int: return self.frame.code.firstlineno def getsource(self, astcache=None) -> Optional["Source"]: - """ return failing source code. """ + """Return failing source code.""" # we use the passed in astcache to not reparse asttrees # within exception info printing source = self.frame.code.fullsource @@ -251,19 +252,19 @@ def getsource(self, astcache=None) -> Optional["Source"]: source = property(getsource) - def ishidden(self): - """ return True if the current frame has a var __tracebackhide__ - resolving to True. + def ishidden(self) -> bool: + """Return True if the current frame has a var __tracebackhide__ + resolving to True. - If __tracebackhide__ is a callable, it gets called with the - ExceptionInfo instance and can decide whether to hide the traceback. + If __tracebackhide__ is a callable, it gets called with the + ExceptionInfo instance and can decide whether to hide the traceback. - mostly for internal use + Mostly for internal use. """ f = self.frame tbh = f.f_locals.get( "__tracebackhide__", f.f_globals.get("__tracebackhide__", False) - ) + ) # type: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] if tbh and callable(tbh): return tbh(None if self._excinfo is None else self._excinfo()) return tbh @@ -280,21 +281,19 @@ def __str__(self) -> str: @property def name(self) -> str: - """ co_name of underlying code """ + """co_name of underlying code.""" return self.frame.code.raw.co_name class Traceback(List[TracebackEntry]): - """ Traceback objects encapsulate and offer higher level - access to Traceback entries. - """ + """Traceback objects encapsulate and offer higher level access to Traceback entries.""" def __init__( self, tb: Union[TracebackType, Iterable[TracebackEntry]], - excinfo: Optional["ReferenceType[ExceptionInfo]"] = None, + excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, ) -> None: - """ initialize from given python traceback object and ExceptionInfo """ + """Initialize from given python traceback object and ExceptionInfo.""" self._excinfo = excinfo if isinstance(tb, TracebackType): @@ -313,16 +312,16 @@ def cut( path=None, lineno: Optional[int] = None, firstlineno: Optional[int] = None, - excludepath=None, + excludepath: Optional[py.path.local] = None, ) -> "Traceback": - """ return a Traceback instance wrapping part of this Traceback + """Return a Traceback instance wrapping part of this Traceback. - by providing any combination of path, lineno and firstlineno, the - first frame to start the to-be-returned traceback is determined + By providing any combination of path, lineno and firstlineno, the + first frame to start the to-be-returned traceback is determined. - this allows cutting the first part of a Traceback instance e.g. - for formatting reasons (removing some uninteresting bits that deal - with handling of the exception/traceback) + This allows cutting the first part of a Traceback instance e.g. + for formatting reasons (removing some uninteresting bits that deal + with handling of the exception/traceback). """ for x in self: code = x.frame.code @@ -359,21 +358,19 @@ def __getitem__( # noqa: F811 def filter( self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden() ) -> "Traceback": - """ return a Traceback instance with certain items removed + """Return a Traceback instance with certain items removed - fn is a function that gets a single argument, a TracebackEntry - instance, and should return True when the item should be added - to the Traceback, False when not + fn is a function that gets a single argument, a TracebackEntry + instance, and should return True when the item should be added + to the Traceback, False when not. - by default this removes all the TracebackEntries which are hidden - (see ishidden() above) + By default this removes all the TracebackEntries which are hidden + (see ishidden() above). """ return Traceback(filter(fn, self), self._excinfo) def getcrashentry(self) -> TracebackEntry: - """ return last non-hidden traceback entry that lead - to the exception of a traceback. - """ + """Return last non-hidden traceback entry that lead to the exception of a traceback.""" for i in range(-1, -len(self) - 1, -1): entry = self[i] if not entry.ishidden(): @@ -381,9 +378,8 @@ def getcrashentry(self) -> TracebackEntry: return self[-1] def recursionindex(self) -> Optional[int]: - """ return the index of the frame/TracebackEntry where recursion - originates if appropriate, None if no recursion occurred - """ + """Return the index of the frame/TracebackEntry where recursion originates if + appropriate, None if no recursion occurred.""" cache = {} # type: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] for i, entry in enumerate(self): # id for the code.raw is needed to work around @@ -414,14 +410,12 @@ def recursionindex(self) -> Optional[int]: ) -_E = TypeVar("_E", bound=BaseException) +_E = TypeVar("_E", bound=BaseException, covariant=True) @attr.s(repr=False) class ExceptionInfo(Generic[_E]): - """ wraps sys.exc_info() objects and offers - help for navigating the traceback. - """ + """Wraps sys.exc_info() objects and offers help for navigating the traceback.""" _assert_start_repr = "AssertionError('assert " @@ -435,13 +429,12 @@ def from_exc_info( exc_info: Tuple["Type[_E]", "_E", TracebackType], exprinfo: Optional[str] = None, ) -> "ExceptionInfo[_E]": - """returns an ExceptionInfo for an existing exc_info tuple. + """Returns an ExceptionInfo for an existing exc_info tuple. .. warning:: Experimental API - :param exprinfo: a text string helping to determine if we should strip ``AssertionError`` from the output, defaults to the exception message/``__str__()`` @@ -460,13 +453,12 @@ def from_exc_info( def from_current( cls, exprinfo: Optional[str] = None ) -> "ExceptionInfo[BaseException]": - """returns an ExceptionInfo matching the current traceback + """Returns an ExceptionInfo matching the current traceback. .. warning:: Experimental API - :param exprinfo: a text string helping to determine if we should strip ``AssertionError`` from the output, defaults to the exception message/``__str__()`` @@ -480,8 +472,7 @@ def from_current( @classmethod def for_later(cls) -> "ExceptionInfo[_E]": - """return an unfilled ExceptionInfo - """ + """Return an unfilled ExceptionInfo.""" return cls(None) def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None: @@ -491,7 +482,7 @@ def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None: @property def type(self) -> "Type[_E]": - """the exception class""" + """The exception class.""" assert ( self._excinfo is not None ), ".type can only be used after the context manager exits" @@ -499,7 +490,7 @@ def type(self) -> "Type[_E]": @property def value(self) -> _E: - """the exception value""" + """The exception value.""" assert ( self._excinfo is not None ), ".value can only be used after the context manager exits" @@ -507,7 +498,7 @@ def value(self) -> _E: @property def tb(self) -> TracebackType: - """the exception raw traceback""" + """The exception raw traceback.""" assert ( self._excinfo is not None ), ".tb can only be used after the context manager exits" @@ -515,7 +506,7 @@ def tb(self) -> TracebackType: @property def typename(self) -> str: - """the type name of the exception""" + """The type name of the exception.""" assert ( self._excinfo is not None ), ".typename can only be used after the context manager exits" @@ -523,7 +514,7 @@ def typename(self) -> str: @property def traceback(self) -> Traceback: - """the traceback""" + """The traceback.""" if self._traceback is None: self._traceback = Traceback(self.tb, excinfo=ref(self)) return self._traceback @@ -540,12 +531,12 @@ def __repr__(self) -> str: ) def exconly(self, tryshort: bool = False) -> str: - """ return the exception as a string + """Return the exception as a string. - when 'tryshort' resolves to True, and the exception is a - _pytest._code._AssertionError, only the actual exception part of - the exception representation is returned (so 'AssertionError: ' is - removed from the beginning) + When 'tryshort' resolves to True, and the exception is a + _pytest._code._AssertionError, only the actual exception part of + the exception representation is returned (so 'AssertionError: ' is + removed from the beginning). """ lines = format_exception_only(self.type, self.value) text = "".join(lines) @@ -580,8 +571,7 @@ def getrepr( truncate_locals: bool = True, chain: bool = True, ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]: - """ - Return str()able representation of this exception info. + """Return str()able representation of this exception info. :param bool showlocals: Show locals per traceback entry. @@ -630,11 +620,10 @@ def getrepr( return fmt.repr_excinfo(self) def match(self, regexp: "Union[str, Pattern]") -> "Literal[True]": - """ - Check whether the regular expression `regexp` matches the string + """Check whether the regular expression `regexp` matches the string representation of the exception using :func:`python:re.search`. - If it matches `True` is returned. - If it doesn't match an `AssertionError` is raised. + + If it matches `True` is returned, otherwise an `AssertionError` is raised. """ __tracebackhide__ = True assert re.search( @@ -646,7 +635,7 @@ def match(self, regexp: "Union[str, Pattern]") -> "Literal[True]": @attr.s class FormattedExcinfo: - """ presenting information about failing Functions and Generators. """ + """Presenting information about failing Functions and Generators.""" # for traceback entries flow_marker = ">" @@ -697,7 +686,7 @@ def get_source( excinfo: Optional[ExceptionInfo] = None, short: bool = False, ) -> List[str]: - """ return formatted and marked up source lines. """ + """Return formatted and marked up source lines.""" lines = [] if source is None or line_index >= len(source.lines): source = Source("???") @@ -938,7 +927,7 @@ class ExceptionRepr(TerminalRepr): reprcrash = None # type: Optional[ReprFileLocation] reprtraceback = None # type: ReprTraceback - def __attrs_post_init__(self): + def __attrs_post_init__(self) -> None: self.sections = [] # type: List[Tuple[str, str, str]] def addsection(self, name: str, content: str, sep: str = "-") -> None: @@ -958,7 +947,7 @@ class ExceptionChainRepr(ExceptionRepr): ] ) - def __attrs_post_init__(self): + def __attrs_post_init__(self) -> None: super().__attrs_post_init__() # reprcrash and reprtraceback of the outermost (the newest) exception # in the chain @@ -1160,8 +1149,9 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line("") -def getfslineno(obj: Any) -> Tuple[Union[str, py.path.local], int]: - """ Return source location (path, lineno) for the given object. +def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: + """Return source location (path, lineno) for the given object. + If the source cannot be determined return ("", -1). The line number is 0-based. @@ -1171,13 +1161,13 @@ def getfslineno(obj: Any) -> Tuple[Union[str, py.path.local], int]: # in 6ec13a2b9. It ("place_as") appears to be something very custom. obj = get_real_func(obj) if hasattr(obj, "place_as"): - obj = obj.place_as + obj = obj.place_as # type: ignore[attr-defined] try: code = Code(obj) except TypeError: try: - fn = inspect.getsourcefile(obj) or inspect.getfile(obj) + fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type] except TypeError: return "", -1 @@ -1189,8 +1179,8 @@ def getfslineno(obj: Any) -> Tuple[Union[str, py.path.local], int]: except OSError: pass return fspath, lineno - else: - return code.path, code.firstlineno + + return code.path, code.firstlineno # relative paths that we use to filter traceback entries from appearing to the user; From c3864bc12b1652c9afd50a7ae059a2fc1ea193be Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Jul 2020 11:51:50 +0300 Subject: [PATCH 467/823] code/code: remove Frame.is_true() method Really odd one, let's just inline it. --- src/_pytest/_code/code.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index aa28fea187d..9c724c8b653 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -158,9 +158,6 @@ def repr(self, object: object) -> str: """Return a 'safe' (non-recursive, one-line) string repr for 'object'.""" return saferepr(object) - def is_true(self, object): - return object - def getargs(self, var: bool = False): """Return a list of tuples (name, value) for all arguments. @@ -393,12 +390,10 @@ def recursionindex(self) -> Optional[int]: f = entry.frame loc = f.f_locals for otherloc in values: - if f.is_true( - f.eval( - co_equal, - __recursioncache_locals_1=loc, - __recursioncache_locals_2=otherloc, - ) + if f.eval( + co_equal, + __recursioncache_locals_1=loc, + __recursioncache_locals_2=otherloc, ): return i values.append(entry.frame.f_locals) From 85ef2bf698aa0f3732d161e99104f9df40a632bc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Jul 2020 12:00:14 +0300 Subject: [PATCH 468/823] code/code: remove Frame.exec_() method Not used. --- src/_pytest/_code/code.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 9c724c8b653..c1e6f49f3b9 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -145,15 +145,6 @@ def eval(self, code, **vars): f_locals.update(vars) return eval(code, self.f_globals, f_locals) - def exec_(self, code, **vars) -> None: - """Exec 'code' in the frame. - - 'vars' are optional; additional local variables. - """ - f_locals = self.f_locals.copy() - f_locals.update(vars) - exec(code, self.f_globals, f_locals) - def repr(self, object: object) -> str: """Return a 'safe' (non-recursive, one-line) string repr for 'object'.""" return saferepr(object) From c8676002a7e11c7d4dcc39abdeb907826b244129 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Jul 2020 12:05:15 +0300 Subject: [PATCH 469/823] code/code: remove redundant __ne__ implementation This implementation is the default when __eq__ is implemented. --- src/_pytest/_code/code.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index c1e6f49f3b9..eb85d941cd6 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -69,9 +69,6 @@ def __eq__(self, other): # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore - def __ne__(self, other): - return not self == other - @property def path(self) -> Union[py.path.local, str]: """Return a path object pointing to source code (or a str in case From 7934ac280f2ddd956086ad5c078dfe901c94d67e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Jul 2020 13:02:00 +0300 Subject: [PATCH 470/823] Add changelog entry for Frame removals --- changelog/7472.breaking.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/7472.breaking.rst diff --git a/changelog/7472.breaking.rst b/changelog/7472.breaking.rst new file mode 100644 index 00000000000..b76874e3779 --- /dev/null +++ b/changelog/7472.breaking.rst @@ -0,0 +1 @@ +The ``exec_()`` and ``is_true()`` methods of ``_pytest._code.Frame`` have been removed. From c1c5a2b34ad59e778d6cdc022ed03039375478c3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 10 Jul 2020 14:49:10 +0300 Subject: [PATCH 471/823] Add support for NO_COLOR and FORCE_COLOR (#7466) Co-authored-by: Bruno Oliveira --- changelog/7464.feature.rst | 3 +++ doc/en/reference.rst | 29 ++++++++++++++++++++------ src/_pytest/_io/terminalwriter.py | 9 ++++---- testing/io/test_terminalwriter.py | 34 +++++++++++++++++++++++++++---- 4 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 changelog/7464.feature.rst diff --git a/changelog/7464.feature.rst b/changelog/7464.feature.rst new file mode 100644 index 00000000000..db9d3c60415 --- /dev/null +++ b/changelog/7464.feature.rst @@ -0,0 +1,3 @@ +Added support for ``NO_COLOR`` and ``FORCE_COLOR`` environment variables to control colored output. + +For more information, see `the docs `__. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 94a2470953a..86ed89d897a 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -988,10 +988,20 @@ Environment variables that can be used to change pytest's behavior. This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given by the user, see :ref:`adding default options` for more information. +.. envvar:: PYTEST_CURRENT_TEST + +This is not meant to be set by users, but is set by pytest internally with the name of the current test so other +processes can inspect it, see :ref:`pytest current test env` for more information. + .. envvar:: PYTEST_DEBUG When set, pytest will print tracing and debug information. +.. envvar:: PYTEST_DISABLE_PLUGIN_AUTOLOAD + +When set, disables plugin auto-loading through setuptools entrypoints. Only explicitly specified plugins will be +loaded. + .. envvar:: PYTEST_PLUGINS Contains comma-separated list of modules that should be loaded as plugins: @@ -1000,15 +1010,22 @@ Contains comma-separated list of modules that should be loaded as plugins: export PYTEST_PLUGINS=mymodule.plugin,xdist -.. envvar:: PYTEST_DISABLE_PLUGIN_AUTOLOAD +.. envvar:: PY_COLORS -When set, disables plugin auto-loading through setuptools entrypoints. Only explicitly specified plugins will be -loaded. +When set to ``1``, pytest will use color in terminal output. +When set to ``0``, pytest will not use color. +``PY_COLORS`` takes precedence over ``NO_COLOR`` and ``FORCE_COLOR``. -.. envvar:: PYTEST_CURRENT_TEST +.. envvar:: NO_COLOR -This is not meant to be set by users, but is set by pytest internally with the name of the current test so other -processes can inspect it, see :ref:`pytest current test env` for more information. +When set (regardless of value), pytest will not use color in terminal output. +``PY_COLORS`` takes precedence over ``NO_COLOR``, which takes precedence over ``FORCE_COLOR``. +See `no-color.org `__ for other libraries supporting this community standard. + +.. envvar:: FORCE_COLOR + +When set (regardless of value), pytest will use color in terminal output. +``PY_COLORS`` and ``NO_COLOR`` take precedence over ``FORCE_COLOR``. Exceptions ---------- diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 70bb2e2dcd6..0168dc13d4d 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -27,11 +27,12 @@ def should_do_markup(file: TextIO) -> bool: return True if os.environ.get("PY_COLORS") == "0": return False + if "NO_COLOR" in os.environ: + return False + if "FORCE_COLOR" in os.environ: + return True return ( - hasattr(file, "isatty") - and file.isatty() - and os.environ.get("TERM") != "dumb" - and not (sys.platform.startswith("java") and os._name == "nt") + hasattr(file, "isatty") and file.isatty() and os.environ.get("TERM") != "dumb" ) diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py index 94cff307fcd..b36a7bb6a11 100644 --- a/testing/io/test_terminalwriter.py +++ b/testing/io/test_terminalwriter.py @@ -154,8 +154,7 @@ def test_attr_hasmarkup() -> None: assert "\x1b[0m" in s -def test_should_do_markup_PY_COLORS_eq_1(monkeypatch: MonkeyPatch) -> None: - monkeypatch.setitem(os.environ, "PY_COLORS", "1") +def assert_color_set(): file = io.StringIO() tw = terminalwriter.TerminalWriter(file) assert tw.hasmarkup @@ -166,8 +165,7 @@ def test_should_do_markup_PY_COLORS_eq_1(monkeypatch: MonkeyPatch) -> None: assert "\x1b[0m" in s -def test_should_do_markup_PY_COLORS_eq_0(monkeypatch: MonkeyPatch) -> None: - monkeypatch.setitem(os.environ, "PY_COLORS", "0") +def assert_color_not_set(): f = io.StringIO() f.isatty = lambda: True # type: ignore tw = terminalwriter.TerminalWriter(file=f) @@ -177,6 +175,34 @@ def test_should_do_markup_PY_COLORS_eq_0(monkeypatch: MonkeyPatch) -> None: assert s == "hello\n" +def test_should_do_markup_PY_COLORS_eq_1(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setitem(os.environ, "PY_COLORS", "1") + assert_color_set() + + +def test_should_not_do_markup_PY_COLORS_eq_0(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setitem(os.environ, "PY_COLORS", "0") + assert_color_not_set() + + +def test_should_not_do_markup_NO_COLOR(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setitem(os.environ, "NO_COLOR", "1") + assert_color_not_set() + + +def test_should_do_markup_FORCE_COLOR(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setitem(os.environ, "FORCE_COLOR", "1") + assert_color_set() + + +def test_should_not_do_markup_NO_COLOR_and_FORCE_COLOR( + monkeypatch: MonkeyPatch, +) -> None: + monkeypatch.setitem(os.environ, "NO_COLOR", "1") + monkeypatch.setitem(os.environ, "FORCE_COLOR", "1") + assert_color_not_set() + + class TestTerminalWriterLineWidth: def test_init(self) -> None: tw = terminalwriter.TerminalWriter() From 1667d138aa25e5c5aa8d0d55c3fedc13143c20b0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Jul 2020 09:30:04 -0300 Subject: [PATCH 472/823] Use sphinx references for NO_COLOR and FORCE_COLOR in changelog --- changelog/7464.feature.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/changelog/7464.feature.rst b/changelog/7464.feature.rst index db9d3c60415..8b27ee96429 100644 --- a/changelog/7464.feature.rst +++ b/changelog/7464.feature.rst @@ -1,3 +1 @@ -Added support for ``NO_COLOR`` and ``FORCE_COLOR`` environment variables to control colored output. - -For more information, see `the docs `__. +Added support for :envvar:`NO_COLOR` and :envvar:`FORCE_COLOR` environment variables to control colored output. From 906d8496c9e7af420cd604685a92aceb15e53d95 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Jul 2020 09:50:03 -0300 Subject: [PATCH 473/823] New doc role: globalvar for special variables This introduces a new role, `:globalvar:`, so we can mark/reference variables like `pytest_plugins`, `pytestmark`, etc. This besides being useful also makes the documentation look more consistent. --- doc/en/assert.rst | 2 -- doc/en/changelog.rst | 2 +- doc/en/conf.py | 7 +++++++ doc/en/deprecations.rst | 2 +- doc/en/example/markers.rst | 27 ++++++++------------------- doc/en/example/pythoncollection.rst | 2 +- doc/en/fixture.rst | 5 +---- doc/en/plugins.rst | 3 ++- doc/en/reference.rst | 27 +++++++++------------------ doc/en/skipping.rst | 4 ++-- doc/en/warnings.rst | 2 +- doc/en/writing_plugins.rst | 16 ++++++++-------- 12 files changed, 41 insertions(+), 58 deletions(-) diff --git a/doc/en/assert.rst b/doc/en/assert.rst index e39da6e8a27..7e43b07fd75 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -294,8 +294,6 @@ Assertion introspection details ------------------------------- - - Reporting details about a failing assertion is achieved by rewriting assert statements before they are run. Rewritten assert statements put introspection information into the assertion failure message. ``pytest`` only rewrites test diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index d6e07c8e4e9..415bfd7c780 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -2188,7 +2188,7 @@ Features - `#3711 `_: Add the ``--ignore-glob`` parameter to exclude test-modules with Unix shell-style wildcards. - Add the ``collect_ignore_glob`` for ``conftest.py`` to exclude test-modules with Unix shell-style wildcards. + Add the :globalvar:`collect_ignore_glob` for ``conftest.py`` to exclude test-modules with Unix shell-style wildcards. - `#4698 `_: The warning about Python 2.7 and 3.4 not being supported in pytest 5.0 has been removed. diff --git a/doc/en/conf.py b/doc/en/conf.py index 72e2d4f20f1..c631484aa34 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -390,4 +390,11 @@ def setup(app: "sphinx.application.Sphinx") -> None: indextemplate="pair: %s; configuration value", ) + app.add_object_type( + "globalvar", + "globalvar", + objname="global variable interpreted by pytest", + indextemplate="pair: %s; global variable interpreted by pytest", + ) + configure_logging(app) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index ccf31cd8bbf..a2bed186256 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -413,7 +413,7 @@ pytest_plugins in non-top-level conftest files .. versionremoved:: 4.0 -Defining ``pytest_plugins`` is now deprecated in non-top-level conftest.py +Defining :globalvar:`pytest_plugins` is now deprecated in non-top-level conftest.py files because they will activate referenced plugins *globally*, which is surprising because for all other pytest features ``conftest.py`` files are only *active* for tests at or below it. diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 454304679aa..38610ee3a60 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -280,17 +280,18 @@ its test methods: This is equivalent to directly applying the decorator to the two test functions. -Due to legacy reasons, it is possible to set the ``pytestmark`` attribute on a TestClass like this: - -.. code-block:: python +To apply marks at the module level, use the :globalvar:`pytestmark` global variable: import pytest + pytestmark = pytest.mark.webtest +or multiple markers:: - class TestClass: - pytestmark = pytest.mark.webtest + pytestmark = [pytest.mark.webtest, pytest.mark.slowtest] -or if you need to use multiple markers you can use a list: + +Due to legacy reasons, before class decorators were introduced, it is possible to set the +:globalvar:`pytestmark` attribute on a test class like this: .. code-block:: python @@ -298,19 +299,7 @@ or if you need to use multiple markers you can use a list: class TestClass: - pytestmark = [pytest.mark.webtest, pytest.mark.slowtest] - -You can also set a module level marker:: - - import pytest - pytestmark = pytest.mark.webtest - -or multiple markers:: - - pytestmark = [pytest.mark.webtest, pytest.mark.slowtest] - -in which case markers will be applied (in left-to-right order) to -all functions and methods defined in the module. + pytestmark = pytest.mark.webtest .. _`marking individual tests when using parametrize`: diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 85e5da26302..a12e2deaa77 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -299,7 +299,7 @@ file will be left out: ========================== no tests ran in 0.12s =========================== It's also possible to ignore files based on Unix shell-style wildcards by adding -patterns to ``collect_ignore_glob``. +patterns to :globalvar:`collect_ignore_glob`. The following example ``conftest.py`` ignores the file ``setup.py`` and in addition all files that end with ``*_py2.py`` when executed with a Python 3 diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 8d2d063677d..a023621dcc0 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -1213,15 +1213,12 @@ You can specify multiple fixtures like this: def test(): ... -and you may specify fixture usage at the test module level, using -a generic feature of the mark mechanism: +and you may specify fixture usage at the test module level using :globalvar:`pytestmark`: .. code-block:: python pytestmark = pytest.mark.usefixtures("cleandir") -Note that the assigned variable *must* be called ``pytestmark``, assigning e.g. -``foomark`` will not activate the fixtures. It is also possible to put fixtures required by all tests in your project into an ini-file: diff --git a/doc/en/plugins.rst b/doc/en/plugins.rst index c3a93141971..855b597392b 100644 --- a/doc/en/plugins.rst +++ b/doc/en/plugins.rst @@ -70,7 +70,7 @@ You may also discover more plugins through a `pytest- pypi.org search`_. Requiring/Loading plugins in a test module or conftest file ----------------------------------------------------------- -You can require plugins in a test module or a conftest file like this: +You can require plugins in a test module or a conftest file using :globalvar:`pytest_plugins`: .. code-block:: python @@ -80,6 +80,7 @@ When the test module or conftest plugin is loaded the specified plugins will be loaded as well. .. note:: + Requiring plugins using a ``pytest_plugins`` variable in non-root ``conftest.py`` files is deprecated. See :ref:`full explanation ` diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 86ed89d897a..d81ba9bc7ea 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -900,14 +900,14 @@ Result used within :ref:`hook wrappers `. .. automethod:: pluggy.callers._Result.get_result .. automethod:: pluggy.callers._Result.force_result -Special Variables ------------------ +Global Variables +---------------- -pytest treats some global variables in a special manner when defined in a test module. +pytest treats some global variables in a special manner when defined in a test module or +``conftest.py`` files. -collect_ignore -~~~~~~~~~~~~~~ +.. globalvar:: collect_ignore **Tutorial**: :ref:`customizing-test-collection` @@ -919,8 +919,7 @@ Needs to be ``list[str]``. collect_ignore = ["setup.py"] -collect_ignore_glob -~~~~~~~~~~~~~~~~~~~ +.. globalvar:: collect_ignore_glob **Tutorial**: :ref:`customizing-test-collection` @@ -933,8 +932,7 @@ contain glob patterns. collect_ignore_glob = ["*_ignore.py"] -pytest_plugins -~~~~~~~~~~~~~~ +.. globalvar:: pytest_plugins **Tutorial**: :ref:`available installable plugins` @@ -950,13 +948,12 @@ Can be either a ``str`` or ``Sequence[str]``. pytest_plugins = ("myapp.testsupport.tools", "myapp.testsupport.regression") -pytestmark -~~~~~~~~~~ +.. globalvar:: pytestmark **Tutorial**: :ref:`scoped-marking` Can be declared at the **global** level in *test modules* to apply one or more :ref:`marks ` to all -test functions and methods. Can be either a single mark or a list of marks. +test functions and methods. Can be either a single mark or a list of marks (applied in left-to-right order). .. code-block:: python @@ -971,12 +968,6 @@ test functions and methods. Can be either a single mark or a list of marks. pytestmark = [pytest.mark.integration, pytest.mark.slow] -PYTEST_DONT_REWRITE (module docstring) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The text ``PYTEST_DONT_REWRITE`` can be add to any **module docstring** to disable -:ref:`assertion rewriting ` for that module. - Environment Variables --------------------- diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 951a56566d3..c4e7d4a0a05 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -152,8 +152,8 @@ You can use the ``skipif`` marker (as any other marker) on classes: If the condition is ``True``, this marker will produce a skip result for each of the test methods of that class. -If you want to skip all test functions of a module, you may use -the ``pytestmark`` name on the global level: +If you want to skip all test functions of a module, you may use the +:globalvar:`pytestmark` global: .. code-block:: python diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 6a37b2ad0b9..30ea529650c 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -117,7 +117,7 @@ Filters applied using a mark take precedence over filters passed on the command by the ``filterwarnings`` ini option. You may apply a filter to all tests of a class by using the ``filterwarnings`` mark as a class -decorator or to all tests in a module by setting the ``pytestmark`` variable: +decorator or to all tests in a module by setting the :globalvar:`pytestmark` variable: .. code-block:: python diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index f3e4cbd23ee..27ef40e5b39 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -52,7 +52,7 @@ Plugin discovery order at tool startup your ``conftest.py`` file in the top level test or project root directory. * by recursively loading all plugins specified by the - ``pytest_plugins`` variable in ``conftest.py`` files + :globalvar:`pytest_plugins` variable in ``conftest.py`` files .. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/ @@ -227,7 +227,7 @@ import ``helper.py`` normally. The contents of Requiring/Loading plugins in a test module or conftest file ----------------------------------------------------------- -You can require plugins in a test module or a ``conftest.py`` file like this: +You can require plugins in a test module or a ``conftest.py`` file using :globalvar:`pytest_plugins`: .. code-block:: python @@ -241,31 +241,31 @@ application modules: pytest_plugins = "myapp.testsupport.myplugin" -``pytest_plugins`` variables are processed recursively, so note that in the example above -if ``myapp.testsupport.myplugin`` also declares ``pytest_plugins``, the contents +:globalvar:`pytest_plugins` are processed recursively, so note that in the example above +if ``myapp.testsupport.myplugin`` also declares :globalvar:`pytest_plugins`, the contents of the variable will also be loaded as plugins, and so on. .. _`requiring plugins in non-root conftests`: .. note:: - Requiring plugins using a ``pytest_plugins`` variable in non-root + Requiring plugins using :globalvar:`pytest_plugins` variable in non-root ``conftest.py`` files is deprecated. This is important because ``conftest.py`` files implement per-directory hook implementations, but once a plugin is imported, it will affect the entire directory tree. In order to avoid confusion, defining - ``pytest_plugins`` in any ``conftest.py`` file which is not located in the + :globalvar:`pytest_plugins` in any ``conftest.py`` file which is not located in the tests root directory is deprecated, and will raise a warning. This mechanism makes it easy to share fixtures within applications or even external applications without the need to create external plugins using the ``setuptools``'s entry point technique. -Plugins imported by ``pytest_plugins`` will also automatically be marked +Plugins imported by :globalvar:`pytest_plugins` will also automatically be marked for assertion rewriting (see :func:`pytest.register_assert_rewrite`). However for this to have any effect the module must not be imported already; if it was already imported at the time the -``pytest_plugins`` statement is processed, a warning will result and +:globalvar:`pytest_plugins` statement is processed, a warning will result and assertions inside the plugin will not be rewritten. To fix this you can either call :func:`pytest.register_assert_rewrite` yourself before the module is imported, or you can arrange the code to delay the From c1ca42b5c264b4ea1b4591bc12e08bd740ba4987 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 10 Jul 2020 22:29:06 +0300 Subject: [PATCH 474/823] mark/structure: fix pylint complaining that builtin marks are not callable --- src/_pytest/mark/structures.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 0c9344f3fc0..ffa6b78a24b 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -4,6 +4,7 @@ import warnings from typing import Any from typing import Callable +from typing import cast from typing import Iterable from typing import List from typing import Mapping @@ -467,12 +468,14 @@ def test_function(): # See TYPE_CHECKING above. if TYPE_CHECKING: - skip = None # type: _SkipMarkDecorator - skipif = None # type: _SkipifMarkDecorator - xfail = None # type: _XfailMarkDecorator - parametrize = None # type: _ParametrizeMarkDecorator - usefixtures = None # type: _UsefixturesMarkDecorator - filterwarnings = None # type: _FilterwarningsMarkDecorator + # Using casts instead of type comments intentionally - issue #7473. + # TODO(py36): Change to builtin annotation syntax. + skip = cast(_SkipMarkDecorator, None) + skipif = cast(_SkipifMarkDecorator, None) + xfail = cast(_XfailMarkDecorator, None) + parametrize = cast(_ParametrizeMarkDecorator, None) + usefixtures = cast(_UsefixturesMarkDecorator, None) + filterwarnings = cast(_FilterwarningsMarkDecorator, None) def __getattr__(self, name: str) -> MarkDecorator: if name[0] == "_": From 113339b029999d29be0d6a2e442391956f60cc31 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 11 Jul 2020 18:51:47 +0300 Subject: [PATCH 475/823] terminalwriter: bring back handling of printing characters not supported by stdout --- src/_pytest/_io/terminalwriter.py | 13 ++++++++++++- testing/io/test_terminalwriter.py | 9 +++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 0168dc13d4d..5ffc550db28 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -149,7 +149,18 @@ def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None: msg = self.markup(msg, **markup) - self._file.write(msg) + try: + self._file.write(msg) + except UnicodeEncodeError: + # Some environments don't support printing general Unicode + # strings, due to misconfiguration or otherwise; in that case, + # print the string escaped to ASCII. + # When the Unicode situation improves we should consider + # letting the error propagate instead of masking it (see #7475 + # for one brief attempt). + msg = msg.encode("unicode-escape").decode("ascii") + self._file.write(msg) + if flush: self.flush() diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py index b36a7bb6a11..db0ccf06a40 100644 --- a/testing/io/test_terminalwriter.py +++ b/testing/io/test_terminalwriter.py @@ -49,6 +49,15 @@ def isatty(self): assert not tw.hasmarkup +def test_terminalwriter_not_unicode() -> None: + """If the file doesn't support Unicode, the string is unicode-escaped (#7475).""" + buffer = io.BytesIO() + file = io.TextIOWrapper(buffer, encoding="cp1252") + tw = terminalwriter.TerminalWriter(file) + tw.write("hello 🌀 wôrld אבג", flush=True) + assert buffer.getvalue() == br"hello \U0001f300 w\xf4rld \u05d0\u05d1\u05d2" + + win32 = int(sys.platform == "win32") From 7f467ebc9af3066e1154f2c9929cc5bcdf632bbf Mon Sep 17 00:00:00 2001 From: Simon K Date: Sat, 11 Jul 2020 17:40:28 +0100 Subject: [PATCH 476/823] Create subdirectories if they do not exist when specified for log file (#7468) Co-authored-by: Bruno Oliveira --- changelog/7467.improvement.rst | 1 + src/_pytest/logging.py | 6 ++++++ testing/logging/test_reporting.py | 9 +++++++++ 3 files changed, 16 insertions(+) create mode 100644 changelog/7467.improvement.rst diff --git a/changelog/7467.improvement.rst b/changelog/7467.improvement.rst new file mode 100644 index 00000000000..b7cf3b4d8aa --- /dev/null +++ b/changelog/7467.improvement.rst @@ -0,0 +1 @@ +``--log-file`` CLI option and ``log_file`` ini marker now create subdirectories if needed. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 1df0643a6df..11031f2f229 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -531,11 +531,17 @@ def __init__(self, config: Config) -> None: # File logging. self.log_file_level = get_log_level_for_setting(config, "log_file_level") log_file = get_option_ini(config, "log_file") or os.devnull + if log_file != os.devnull: + directory = os.path.dirname(os.path.abspath(log_file)) + if not os.path.isdir(directory): + os.makedirs(directory) + self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8") log_file_format = get_option_ini(config, "log_file_format", "log_format") log_file_date_format = get_option_ini( config, "log_file_date_format", "log_date_format" ) + log_file_formatter = logging.Formatter( log_file_format, datefmt=log_file_date_format ) diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index bbdf28b389a..32224325884 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -5,6 +5,7 @@ import pytest from _pytest.capture import CaptureManager +from _pytest.config import ExitCode from _pytest.pytester import Testdir from _pytest.terminal import TerminalReporter @@ -1152,3 +1153,11 @@ def test_bad_log(monkeypatch): ) result = testdir.runpytest() result.assert_outcomes(passed=1) + + +def test_log_file_cli_subdirectories_are_successfully_created(testdir): + path = testdir.makepyfile(""" def test_logger(): pass """) + expected = os.path.join(os.path.dirname(str(path)), "foo", "bar") + result = testdir.runpytest("--log-file=foo/bar/logf.log") + assert "logf.log" in os.listdir(expected) + assert result.ret == ExitCode.OK From 789654dfe2a1f093fa70a2d06f856997184de9df Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 11 Jul 2020 15:41:10 -0300 Subject: [PATCH 477/823] Small fixes/updates to the 6.0.0rc1 CHANGELOG - Grammar fixes - Moved a few sections from Features to Improvements - Used internal doc links when appropriate --- doc/en/changelog.rst | 106 ++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 62 deletions(-) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 415bfd7c780..ef5528f69c6 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -82,7 +82,7 @@ Breaking Changes the test suite. -- `#7122 `_: Expressions given to the ``-m`` and ``-k`` options are no longer evaluated using Python's ``eval()``. +- `#7122 `_: Expressions given to the ``-m`` and ``-k`` options are no longer evaluated using Python's :func:`eval`. The format supports ``or``, ``and``, ``not``, parenthesis and general identifiers to match against. Python constants, keywords or other operators are no longer evaluated differently. @@ -113,7 +113,7 @@ Breaking Changes - `#7226 `_: Removed the unused ``args`` parameter from ``pytest.Function.__init__``. -- `#7418 `_: Remove the `pytest_doctest_prepare_content` hook specification. This hook +- `#7418 `_: Removed the `pytest_doctest_prepare_content` hook specification. This hook hasn't been triggered by pytest for at least 10 years. @@ -134,17 +134,6 @@ Breaking Changes Deprecations ------------ -- `#6981 `_: Deprecate the ``pytest.collect`` module as it's just aliases into ``pytest``. - - -- `#7097 `_: The ``pytest._fillfuncargs`` function is now deprecated. This function was kept - for backward compatibility with an older plugin. - - It's functionality is not meant to be used directly, but if you must replace - it, use `function._request._fillfixtures()` instead, though note this is not - a public API and may break in the future. - - - `#7210 `_: The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'`` instead. @@ -152,7 +141,6 @@ Deprecations if you use this and want a replacement. - Features -------- @@ -192,22 +180,6 @@ Features This hook is meant to replace `pytest_warning_captured`, which will be removed in a future release. -- `#6285 `_: Exposed the `pytest.FixtureLookupError` exception which is raised by `request.getfixturevalue()` - (where `request` is a `FixtureRequest` fixture) when a fixture with the given name cannot be returned. - - -- `#6433 `_: If an error is encountered while formatting the message in a logging call, for - example ``logging.warning("oh no!: %s: %s", "first")`` (a second argument is - missing), pytest now propagates the error, likely causing the test to fail. - - Previously, such a mistake would cause an error to be printed to stderr, which - is not displayed by default for passing tests. This change makes the mistake - visible during testing. - - You may supress this behavior temporarily or permanently by setting - ``logging.raiseExceptions = False``. - - - `#6471 `_: New command-line flags: * `--no-header`: disables the initial header, including platform, version, and plugins. @@ -238,17 +210,13 @@ Features You can read more about this option in `the documentation `__. -- `#7305 `_: New ``required_plugins`` configuration option allows the user to specify a list of plugins required for pytest to run. An error is raised if any required plugins are not found when running pytest. - - -- `#7346 `_: Version information as defined by `PEP 440 `_ may now be included when providing plugins to the ``required_plugins`` configuration option. - +- `#7305 `_: New ``required_plugins`` configuration option allows the user to specify a list of plugins, including version information, that are required for pytest to run. An error is raised if any required plugins are not found when running pytest. Improvements ------------ -- `#4375 `_: The ``pytest`` command now supresses the ``BrokenPipeError`` error message that +- `#4375 `_: The ``pytest`` command now suppresses the ``BrokenPipeError`` error message that is printed to stderr when the output of ``pytest`` is piped and and the pipe is closed by the piped-to program (common examples are ``less`` and ``head``). @@ -259,11 +227,27 @@ Improvements - `#4675 `_: Rich comparison for dataclasses and `attrs`-classes is now recursive. +- `#6285 `_: Exposed the `pytest.FixtureLookupError` exception which is raised by `request.getfixturevalue()` + (where `request` is a `FixtureRequest` fixture) when a fixture with the given name cannot be returned. + + +- `#6433 `_: If an error is encountered while formatting the message in a logging call, for + example ``logging.warning("oh no!: %s: %s", "first")`` (a second argument is + missing), pytest now propagates the error, likely causing the test to fail. + + Previously, such a mistake would cause an error to be printed to stderr, which + is not displayed by default for passing tests. This change makes the mistake + visible during testing. + + You may supress this behavior temporarily or permanently by setting + ``logging.raiseExceptions = False``. + + - `#6817 `_: Explicit new-lines in help texts of command-line options are preserved, allowing plugins better control of the help displayed to users. -- `#6940 `_: When using the ``--duration`` option, the terminal message output is now more precise about the number and durations of hidden items. +- `#6940 `_: When using the ``--duration`` option, the terminal message output is now more precise about the number and duration of hidden items. - `#6991 `_: Collected files are displayed after any reports from hooks, e.g. the status from ``--lf``. @@ -275,22 +259,18 @@ Improvements file descriptors would fail or be lost in this case. -- `#7119 `_: Exit with an error if the ``--basetemp`` argument is empty, the current working directory or parent directory of it. +- `#7119 `_: Exit with an error if the ``--basetemp`` argument is empty, is the current working directory or is one of the parent directories. This is done to protect against accidental data loss, as any directory passed to this argument is cleared. -- `#7128 `_: `pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. - +- `#7128 `_: `pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. This is more consistent with how other tools show `--version`. -- `#7133 `_: ``caplog.set_level()`` will now override any :confval:`log_level` set via the CLI or ``.ini``. +- `#7133 `_: :meth:`caplog.set_level() <_pytest.logging.LogCaptureFixture.set_level>` will now override any :confval:`log_level` set via the CLI or configuration file. -- `#7159 `_: When the ``caplog`` fixture is used to change the log level for capturing, - using ``caplog.set_level()`` or ``caplog.at_level()``, it no longer affects - the level of logs that are shown in the "Captured log report" report section. - -- `#7264 `_: The dependency on the ``wcwidth`` package has been removed. +- `#7159 `_: :meth:`caplog.set_level() <_pytest.logging.LogCaptureFixture.set_level>` and :meth:`caplog.at_level() <_pytest.logging.LogCaptureFixture.at_level>` no longer affect + the level of logs that are shown in the *Captured log report* report section. - `#7348 `_: Improve recursive diff report for comparison asserts on dataclasses / attrs. @@ -315,17 +295,17 @@ Improvements Bug Fixes --------- -- `#1120 `_: Fix issue where directories from tmpdir are not removed properly when multiple instances of pytest are running in parallel. +- `#1120 `_: Fix issue where directories from :fixture:`tmpdir` are not removed properly when multiple instances of pytest are running in parallel. -- `#4583 `_: Prevent crashing and provide a user-friendly error when a marker expression (-m) invoking of eval() raises any exception. +- `#4583 `_: Prevent crashing and provide a user-friendly error when a marker expression (`-m`) invoking of :func:`eval` raises any exception. - `#4677 `_: The path shown in the summary report for SKIPPED tests is now always relative. Previously it was sometimes absolute. - `#5456 `_: Fix a possible race condition when trying to remove lock files used to control access to folders - created by ``tmp_path`` and ``tmpdir``. + created by :fixture:`tmp_path` and :fixture:`tmpdir`. - `#6240 `_: Fixes an issue where logging during collection step caused duplication of log @@ -336,10 +316,10 @@ Bug Fixes changed since the start of the session. -- `#6755 `_: Support deleting paths longer than 260 characters on windows created inside tmpdir. +- `#6755 `_: Support deleting paths longer than 260 characters on windows created inside :fixture:`tmpdir`. -- `#6871 `_: Fix crash with captured output when using the :fixture:`capsysbinary fixture `. +- `#6871 `_: Fix crash with captured output when using :fixture:`capsysbinary`. - `#6909 `_: Revert the change introduced by `#6330 `_, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature. @@ -353,22 +333,22 @@ Bug Fixes - `#6924 `_: Ensure a ``unittest.IsolatedAsyncioTestCase`` is actually awaited. -- `#6925 `_: Fix TerminalRepr instances to be hashable again. +- `#6925 `_: Fix `TerminalRepr` instances to be hashable again. -- `#6947 `_: Fix regression where functions registered with ``TestCase.addCleanup`` were not being called on test failures. +- `#6947 `_: Fix regression where functions registered with :meth:`unittest.TestCase.addCleanup` were not being called on test failures. - `#6951 `_: Allow users to still set the deprecated ``TerminalReporter.writer`` attribute. -- `#6956 `_: Prevent pytest from printing ConftestImportFailure traceback to stdout. +- `#6956 `_: Prevent pytest from printing `ConftestImportFailure` traceback to stdout. - `#6991 `_: Fix regressions with `--lf` filtering too much since pytest 5.4. -- `#6992 `_: Revert "tmpdir: clean up indirection via config for factories" #6767 as it breaks pytest-xdist. +- `#6992 `_: Revert "tmpdir: clean up indirection via config for factories" `#6767 `_ as it breaks pytest-xdist. - `#7061 `_: When a yielding fixture fails to yield a value, report a test setup error instead of crashing. @@ -384,7 +364,7 @@ Bug Fixes parameter when Python is called with the ``-bb`` flag. -- `#7143 `_: Fix ``File.from_constructor`` so it forwards extra keyword arguments to the constructor. +- `#7143 `_: Fix :meth:`pytest.File.from_parent` so it forwards extra keyword arguments to the constructor. - `#7145 `_: Classes with broken ``__getattribute__`` methods are displayed correctly during failures. @@ -396,8 +376,7 @@ Bug Fixes - `#7180 `_: Fix ``_is_setup_py`` for files encoded differently than locale. -- `#7215 `_: Fix regression where running with ``--pdb`` would call the ``tearDown`` methods of ``unittest.TestCase`` - subclasses for skipped tests. +- `#7215 `_: Fix regression where running with ``--pdb`` would call :meth:`unittest.TestCase.tearDown` for skipped tests. - `#7253 `_: When using ``pytest.fixture`` on a function directly, as in ``pytest.fixture(func)``, @@ -416,13 +395,13 @@ Bug Fixes Improved Documentation ---------------------- -- `#7202 `_: The development guide now links to the contributing section of the docs and 'RELEASING.rst' on GitHub. +- `#7202 `_: The development guide now links to the contributing section of the docs and `RELEASING.rst` on GitHub. - `#7233 `_: Add a note about ``--strict`` and ``--strict-markers`` and the preference for the latter one. -- `#7345 `_: Explain indirect parametrization and markers for fixtures +- `#7345 `_: Explain indirect parametrization and markers for fixtures. @@ -433,16 +412,19 @@ Trivial/Internal Changes provided explicitly, and is always set. +- `#7264 `_: The dependency on the ``wcwidth`` package has been removed. + + - `#7291 `_: Replaced ``py.iniconfig`` with `iniconfig `__. - `#7295 `_: ``src/_pytest/config/__init__.py`` now uses the ``warnings`` module to report warnings instead of ``sys.stderr.write``. -- `#7356 `_: Remove last internal uses of deprecated "slave" term from old pytest-xdist. +- `#7356 `_: Remove last internal uses of deprecated *slave* term from old ``pytest-xdist``. -- `#7357 `_: py>=1.8.2 is now required. +- `#7357 `_: ``py``>=1.8.2 is now required. pytest 5.4.3 (2020-06-02) From 78f2dc08fa998622bb52edd992e04e09918712da Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 13 Jul 2020 23:17:39 +0300 Subject: [PATCH 478/823] skipping: slight simplification --- src/_pytest/skipping.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 335e10996a2..24c89eb6dea 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -231,11 +231,9 @@ def evaluate_xfail_marks(item: Item) -> Optional[Xfail]: @hookimpl(tryfirst=True) def pytest_runtest_setup(item: Item) -> None: - item._store[skipped_by_mark_key] = False - skipped = evaluate_skip_marks(item) + item._store[skipped_by_mark_key] = skipped is not None if skipped: - item._store[skipped_by_mark_key] = True skip(skipped.reason) if not item.config.option.runxfail: From ccad10a82908d7a12cd6024e00be11af413edf1c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 13 Jul 2020 21:34:07 +0300 Subject: [PATCH 479/823] skipping: fix dynamic xfail mark added in runtest not respected If a test runtest phase (not setup) dynamically adds a pytest.mark.xfail mark to the item, it should be respected, but it wasn't. This regressed in 3e6fe92b7ea3c120e8024a970bf37a7c6c137714 (not released). Fix it by just always refreshing the mark if needed. This is mostly what was done before but in a more roundabout way. --- src/_pytest/skipping.py | 17 ++++++++++------- testing/test_skipping.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 24c89eb6dea..e333e78df9b 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -236,10 +236,9 @@ def pytest_runtest_setup(item: Item) -> None: if skipped: skip(skipped.reason) - if not item.config.option.runxfail: - item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) - if xfailed and not xfailed.run: - xfail("[NOTRUN] " + xfailed.reason) + item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) + if xfailed and not item.config.option.runxfail and not xfailed.run: + xfail("[NOTRUN] " + xfailed.reason) @hookimpl(hookwrapper=True) @@ -248,12 +247,16 @@ def pytest_runtest_call(item: Item) -> Generator[None, None, None]: if xfailed is None: item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) - if not item.config.option.runxfail: - if xfailed and not xfailed.run: - xfail("[NOTRUN] " + xfailed.reason) + 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._store.get(xfailed_key, None) + if xfailed is None: + item._store[xfailed_key] = xfailed = evaluate_xfail_marks(item) + @hookimpl(hookwrapper=True) def pytest_runtest_makereport(item: Item, call: CallInfo[None]): diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 8fceb37aa71..61de0b3e177 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1,6 +1,7 @@ import sys import pytest +from _pytest.pytester import Testdir from _pytest.runner import runtestprotocol from _pytest.skipping import evaluate_skip_marks from _pytest.skipping import evaluate_xfail_marks @@ -425,6 +426,33 @@ def test_this2(arg): result = testdir.runpytest(p) result.stdout.fnmatch_lines(["*1 xfailed*"]) + def test_dynamic_xfail_set_during_runtest_failed(self, testdir: Testdir) -> None: + # Issue #7486. + p = testdir.makepyfile( + """ + import pytest + def test_this(request): + request.node.add_marker(pytest.mark.xfail(reason="xfail")) + assert 0 + """ + ) + result = testdir.runpytest(p) + result.assert_outcomes(xfailed=1) + + def test_dynamic_xfail_set_during_runtest_passed_strict( + self, testdir: Testdir + ) -> None: + # Issue #7486. + p = testdir.makepyfile( + """ + import pytest + def test_this(request): + request.node.add_marker(pytest.mark.xfail(reason="xfail", strict=True)) + """ + ) + result = testdir.runpytest(p) + result.assert_outcomes(failed=1) + @pytest.mark.parametrize( "expected, actual, matchline", [ From 1a73e78698490d3b91bf7a85f9b9864411f64dcb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 14 Jul 2020 01:39:04 +0300 Subject: [PATCH 480/823] mark: fix typing for `@pytest.mark.xfail(raises=...)` --- src/_pytest/mark/structures.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 5a50cded033..c55e04755a4 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -28,6 +28,9 @@ from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning +if TYPE_CHECKING: + from typing import Type + EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" @@ -413,7 +416,9 @@ def __call__( # noqa: F811 *conditions: Union[str, bool], reason: str = ..., run: bool = ..., - raises: Union[BaseException, Tuple[BaseException, ...]] = ..., + raises: Union[ + "Type[BaseException]", Tuple["Type[BaseException]", ...] + ] = ..., strict: bool = ... ) -> MarkDecorator: raise NotImplementedError() From 91f6892e6a68c55adc8c9113b427cf96e5c8ee22 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 14 Jul 2020 14:36:41 +0300 Subject: [PATCH 481/823] testing: add a file for checking no mypy errors We probably something a bit more elaborate in the future but for now it's something to verify fixes and catch regressions. --- testing/typing_checks.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 testing/typing_checks.py diff --git a/testing/typing_checks.py b/testing/typing_checks.py new file mode 100644 index 00000000000..94c66ef5103 --- /dev/null +++ b/testing/typing_checks.py @@ -0,0 +1,12 @@ +"""File for checking typing issues. + +This file is not executed, it is only checked by mypy to ensure that +none of the code triggers any mypy errors. +""" +import pytest + + +# Issue #7488. +@pytest.mark.xfail(raises=RuntimeError) +def check_mark_xfail_raises() -> None: + pass From bc17034a67c2ce9cc9169fd85b65bdd2a92db772 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 14 Jul 2020 13:05:38 +0300 Subject: [PATCH 482/823] Fix typing of params ids callable form The previous typing had an object passed to the user, which they can't do anything with without asserting, which is inconvenient. Change it to Any instead. Note that what comes *back* to pytest (the return value) should be an `object`, because we want to handle arbitrary objects without assuming anything about them. --- src/_pytest/fixtures.py | 14 +++++++------- src/_pytest/mark/structures.py | 2 +- src/_pytest/python.py | 11 ++++++----- testing/typing_checks.py | 12 ++++++++++++ 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 8fd56f8ac28..b24fc5fd335 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -976,7 +976,7 @@ def __init__( ids: Optional[ Union[ Tuple[Union[None, str, float, int, bool], ...], - Callable[[object], Optional[object]], + Callable[[Any], Optional[object]], ] ] = None, ) -> None: @@ -1128,13 +1128,13 @@ def _ensure_immutable_ids( ids: Optional[ Union[ Iterable[Union[None, str, float, int, bool]], - Callable[[object], Optional[object]], + Callable[[Any], Optional[object]], ] ], ) -> Optional[ Union[ Tuple[Union[None, str, float, int, bool], ...], - Callable[[object], Optional[object]], + Callable[[Any], Optional[object]], ] ]: if ids is None: @@ -1180,7 +1180,7 @@ class FixtureFunctionMarker: ids = attr.ib( type=Union[ Tuple[Union[None, str, float, int, bool], ...], - Callable[[object], Optional[object]], + Callable[[Any], Optional[object]], ], default=None, converter=_ensure_immutable_ids, @@ -1223,7 +1223,7 @@ def fixture( ids: Optional[ Union[ Iterable[Union[None, str, float, int, bool]], - Callable[[object], Optional[object]], + Callable[[Any], Optional[object]], ] ] = ..., name: Optional[str] = ... @@ -1241,7 +1241,7 @@ def fixture( # noqa: F811 ids: Optional[ Union[ Iterable[Union[None, str, float, int, bool]], - Callable[[object], Optional[object]], + Callable[[Any], Optional[object]], ] ] = ..., name: Optional[str] = None @@ -1258,7 +1258,7 @@ def fixture( # noqa: F811 ids: Optional[ Union[ Iterable[Union[None, str, float, int, bool]], - Callable[[object], Optional[object]], + Callable[[Any], Optional[object]], ] ] = None, name: Optional[str] = None diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index c55e04755a4..5edeecdd5a4 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -433,7 +433,7 @@ def __call__( # type: ignore[override] ids: Optional[ Union[ Iterable[Union[None, str, float, int, bool]], - Callable[[object], Optional[object]], + Callable[[Any], Optional[object]], ] ] = ..., scope: Optional[_Scope] = ... diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 7209bf1edab..aa817148683 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -11,6 +11,7 @@ from collections import defaultdict from collections.abc import Sequence from functools import partial +from typing import Any from typing import Callable from typing import Dict from typing import Generator @@ -920,7 +921,7 @@ def parametrize( ids: Optional[ Union[ Iterable[Union[None, str, float, int, bool]], - Callable[[object], Optional[object]], + Callable[[Any], Optional[object]], ] ] = None, scope: "Optional[_Scope]" = None, @@ -1040,7 +1041,7 @@ def _resolve_arg_ids( ids: Optional[ Union[ Iterable[Union[None, str, float, int, bool]], - Callable[[object], Optional[object]], + Callable[[Any], Optional[object]], ] ], parameters: typing.Sequence[ParameterSet], @@ -1226,7 +1227,7 @@ def _idval( val: object, argname: str, idx: int, - idfn: Optional[Callable[[object], Optional[object]]], + idfn: Optional[Callable[[Any], Optional[object]]], nodeid: Optional[str], config: Optional[Config], ) -> str: @@ -1266,7 +1267,7 @@ def _idvalset( idx: int, parameterset: ParameterSet, argnames: Iterable[str], - idfn: Optional[Callable[[object], Optional[object]]], + idfn: Optional[Callable[[Any], Optional[object]]], ids: Optional[List[Union[None, str]]], nodeid: Optional[str], config: Optional[Config], @@ -1287,7 +1288,7 @@ def _idvalset( def idmaker( argnames: Iterable[str], parametersets: Iterable[ParameterSet], - idfn: Optional[Callable[[object], Optional[object]]] = None, + idfn: Optional[Callable[[Any], Optional[object]]] = None, ids: Optional[List[Union[None, str]]] = None, config: Optional[Config] = None, nodeid: Optional[str] = None, diff --git a/testing/typing_checks.py b/testing/typing_checks.py index 94c66ef5103..0a6b5ad2841 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -10,3 +10,15 @@ @pytest.mark.xfail(raises=RuntimeError) def check_mark_xfail_raises() -> None: pass + + +# Issue #7494. +@pytest.fixture(params=[(0, 0), (1, 1)], ids=lambda x: str(x[0])) +def check_fixture_ids_callable() -> None: + pass + + +# Issue #7494. +@pytest.mark.parametrize("func", [str, int], ids=lambda x: str(x.__name__)) +def check_parametrize_ids_callable(func) -> None: + pass From 97f560d4d1acaacd827fd01e2e9226fa75b5bc6b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 14 Jul 2020 20:20:23 -0300 Subject: [PATCH 483/823] Clarify 'getfixture' needs to access fixtures by normal means Related to #7497 --- doc/en/doctest.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index bb96ee40925..1963214f7a4 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -206,7 +206,11 @@ It is possible to use fixtures using the ``getfixture`` helper: >>> ... >>> -Also, :ref:`usefixtures` and :ref:`autouse` fixtures are supported +Note that the fixture needs to be defined in a place visible by pytest, for example a `conftest.py` +file or plugin; normal python files containing docstrings are not normally scanned for fixtures +unless explicitly configured by :confval:`python_files`. + +Also, the :ref:`usefixtures ` mark and fixtures marked as :ref:`autouse ` are supported when executing text doctest files. From e7c42ae62b3a6a46b4309bf82fe1adcc19a03da1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 15 Jul 2020 09:25:17 -0300 Subject: [PATCH 484/823] Inaccessible lock files now imply temporary directories can't be removed Fix #7500 Co-authored-by: Ran Benita --- changelog/7491.bugfix.rst | 2 ++ src/_pytest/pathlib.py | 11 ++++++++--- testing/test_pathlib.py | 12 ++++++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 changelog/7491.bugfix.rst diff --git a/changelog/7491.bugfix.rst b/changelog/7491.bugfix.rst new file mode 100644 index 00000000000..5b00a5713b5 --- /dev/null +++ b/changelog/7491.bugfix.rst @@ -0,0 +1,2 @@ +:fixture:`tmpdir` and :fixture:`tmp_path` no longer raise an error if the lock to check for +stale temporary directories is not accessible. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 6a0bf7f6f0e..92ba32082a5 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -286,12 +286,17 @@ def maybe_delete_a_numbered_dir(path: Path) -> None: def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool: - """checks if a lock exists and breaks it if its considered dead""" + """checks if `path` is deletable based on whether the lock file is expired""" if path.is_symlink(): return False lock = get_lock_path(path) - if not lock.exists(): - return True + try: + if not lock.is_file(): + return True + except OSError: + # we might not have access to the lock file at all, in this case assume + # we don't have access to the entire directory (#7491). + return False try: lock_time = lock.stat().st_mtime except Exception: diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index d9d3894f935..2c1a1c021f8 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -358,17 +358,25 @@ def test_get_extended_length_path_str(): def test_suppress_error_removing_lock(tmp_path): - """ensure_deletable should not raise an exception if the lock file cannot be removed (#5456)""" + """ensure_deletable should be resilient if lock file cannot be removed (#5456, #7491)""" path = tmp_path / "dir" path.mkdir() lock = get_lock_path(path) lock.touch() mtime = lock.stat().st_mtime - with unittest.mock.patch.object(Path, "unlink", side_effect=OSError): + with unittest.mock.patch.object(Path, "unlink", side_effect=OSError) as m: assert not ensure_deletable( path, consider_lock_dead_if_created_before=mtime + 30 ) + assert m.call_count == 1 + assert lock.is_file() + + with unittest.mock.patch.object(Path, "is_file", side_effect=OSError) as m: + assert not ensure_deletable( + path, consider_lock_dead_if_created_before=mtime + 30 + ) + assert m.call_count == 1 assert lock.is_file() # check now that we can remove the lock file in normal circumstances From 71ab6236a1d71b5bf0e71830e9498eafdeb3d918 Mon Sep 17 00:00:00 2001 From: Lewis Cowles Date: Wed, 15 Jul 2020 20:26:47 +0100 Subject: [PATCH 485/823] Clearer guidance on pytest.raise(match=...) failure (#7499) --- AUTHORS | 1 + changelog/7489.improvement.rst | 1 + src/_pytest/_code/code.py | 7 ++++--- testing/code/test_excinfo.py | 2 +- testing/python/raises.py | 16 ++++++++++++++-- 5 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 changelog/7489.improvement.rst diff --git a/AUTHORS b/AUTHORS index 4cdf231b146..040214e33bb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -164,6 +164,7 @@ Kyle Altendorf Lawrence Mitchell Lee Kamentsky Lev Maximov +Lewis Cowles Llandy Riveron Del Risco Loic Esteve Lukas Bednar diff --git a/changelog/7489.improvement.rst b/changelog/7489.improvement.rst new file mode 100644 index 00000000000..218342f2d29 --- /dev/null +++ b/changelog/7489.improvement.rst @@ -0,0 +1 @@ +The :func:`pytest.raises` function has a clearer error message when ``match`` equals the obtained string but is not a regex match. In this case it is suggested to escape the regex. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index eb85d941cd6..218b5ad6311 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -609,9 +609,10 @@ def match(self, regexp: "Union[str, Pattern]") -> "Literal[True]": If it matches `True` is returned, otherwise an `AssertionError` is raised. """ __tracebackhide__ = True - assert re.search( - regexp, str(self.value) - ), "Pattern {!r} does not match {!r}".format(regexp, str(self.value)) + msg = "Regex pattern {!r} does not match {!r}." + if regexp == str(self.value): + msg += " Did you mean to `re.escape()` the regex?" + assert re.search(regexp, str(self.value)), msg.format(regexp, str(self.value)) # Return True to allow for "assert excinfo.match()". return True diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 060f52cc7a0..78b55e26e01 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -423,7 +423,7 @@ def test_division_zero(): result = testdir.runpytest() assert result.ret != 0 - exc_msg = "Pattern '[[]123[]]+' does not match 'division by zero'" + exc_msg = "Regex pattern '[[]123[]]+' does not match 'division by zero'." result.stdout.fnmatch_lines(["E * AssertionError: {}".format(exc_msg)]) result.stdout.no_fnmatch_line("*__tracebackhide__ = True*") diff --git a/testing/python/raises.py b/testing/python/raises.py index 3f378d015a6..12d44495c99 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -197,7 +197,7 @@ def test_raises_match(self) -> None: int("asdf") msg = "with base 16" - expr = "Pattern {!r} does not match \"invalid literal for int() with base 10: 'asdf'\"".format( + expr = "Regex pattern {!r} does not match \"invalid literal for int() with base 10: 'asdf'\".".format( msg ) with pytest.raises(AssertionError, match=re.escape(expr)): @@ -223,7 +223,19 @@ def test_match_failure_string_quoting(self): with pytest.raises(AssertionError, match="'foo"): raise AssertionError("'bar") (msg,) = excinfo.value.args - assert msg == 'Pattern "\'foo" does not match "\'bar"' + assert msg == 'Regex pattern "\'foo" does not match "\'bar".' + + def test_match_failure_exact_string_message(self): + message = "Oh here is a message with (42) numbers in parameters" + with pytest.raises(AssertionError) as excinfo: + with pytest.raises(AssertionError, match=message): + raise AssertionError(message) + (msg,) = excinfo.value.args + assert msg == ( + "Regex pattern 'Oh here is a message with (42) numbers in " + "parameters' does not match 'Oh here is a message with (42) " + "numbers in parameters'. Did you mean to `re.escape()` the regex?" + ) def test_raises_match_wrong_type(self): """Raising an exception with the wrong type and match= given. From 65b014a117688c28952a00af2595b8a3a30ac625 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 17 Jul 2020 15:59:35 +0300 Subject: [PATCH 486/823] docs: support Sphinx>=3.1 and require it Previously was restricted to >=1.8.2,<2.1, but newer versions have some nice improvements I'd like to be able to use in upcoming changes. Changelog: https://www.sphinx-doc.org/en/master/changes.html#release-3-1-0-released-jun-08-2020 There are two issues that came up: 1. `highlightlang` is deprecated for `highlight`. 2. Doesn't like having two `automethod` generated for the same `Metafunc.parametrize` method. Gives this warning: `pytest/doc/en/reference.rst:846: WARNING: duplicate object description of _pytest.python.Metafunc.parametrize, other instance in reference, use :noindex: for one of them` To work around this I make `pytest.mark.parametrize` link to `Metafunc.parametrize` instead of repeating it. --- doc/en/goodpractices.rst | 2 +- doc/en/reference.rst | 2 +- doc/en/requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/goodpractices.rst b/doc/en/goodpractices.rst index ee5674fd6d8..4b3c0af10a6 100644 --- a/doc/en/goodpractices.rst +++ b/doc/en/goodpractices.rst @@ -1,4 +1,4 @@ -.. highlightlang:: python +.. highlight:: python .. _`goodpractices`: Good Integration Practices diff --git a/doc/en/reference.rst b/doc/en/reference.rst index d81ba9bc7ea..005014d84d4 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -138,7 +138,7 @@ pytest.mark.parametrize **Tutorial**: :doc:`parametrize`. -.. automethod:: _pytest.python.Metafunc.parametrize +This mark has the same signature as :py:meth:`_pytest.python.Metafunc.parametrize`; see there. .. _`pytest.mark.skip ref`: diff --git a/doc/en/requirements.txt b/doc/en/requirements.txt index 1e5e7efdc76..fa37acfb447 100644 --- a/doc/en/requirements.txt +++ b/doc/en/requirements.txt @@ -1,5 +1,5 @@ pallets-sphinx-themes pygments-pytest>=1.1.0 sphinx-removed-in>=0.2.0 -sphinx>=1.8.2,<2.1 +sphinx>=3.1,<4 sphinxcontrib-trio From 25b56e9c6965da3cb900e03f92ca2ff7ff90fd0b Mon Sep 17 00:00:00 2001 From: Debi Mishra Date: Mon, 20 Jul 2020 00:59:19 +0530 Subject: [PATCH 487/823] docs: Add a note about -q option used in getting started guide --- AUTHORS | 1 + changelog/7441.doc.rst | 1 + doc/en/getting-started.rst | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 changelog/7441.doc.rst diff --git a/AUTHORS b/AUTHORS index 040214e33bb..b28e5613389 100644 --- a/AUTHORS +++ b/AUTHORS @@ -81,6 +81,7 @@ David Paul Röthlisberger David Szotten David Vierra Daw-Ran Liou +Debi Mishra Denis Kirisov Dhiren Serai Diego Russo diff --git a/changelog/7441.doc.rst b/changelog/7441.doc.rst new file mode 100644 index 00000000000..d7c770e995d --- /dev/null +++ b/changelog/7441.doc.rst @@ -0,0 +1 @@ +Add a note about ``-q`` option used in getting started guide. diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 0424faa8825..38adf68e0bd 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -112,6 +112,10 @@ Execute the test function with “quiet” reporting mode: . [100%] 1 passed in 0.12s +.. note:: + + The ``-q/--quiet`` flag keeps the output brief in this and following examples. + Group multiple tests in a class -------------------------------------------------------------- From fbeb36226f7286760116c448f9a2b33401b48383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Mon, 20 Jul 2020 13:41:28 +0200 Subject: [PATCH 488/823] List pytest_warning_captured in deprecated things for 6.0.0rc1 --- doc/en/changelog.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index ef5528f69c6..5492f80a308 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -140,6 +140,8 @@ Deprecations The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue if you use this and want a replacement. +- `#4049 `_: ``pytest_warning_captured`` is deprecated in favor of the ``pytest_warning_recorded`` hook. + Features -------- @@ -177,7 +179,7 @@ Features - `#4049 `_: Introduced a new hook named `pytest_warning_recorded` to convey information about warnings captured by the internal `pytest` warnings plugin. - This hook is meant to replace `pytest_warning_captured`, which will be removed in a future release. + This hook is meant to replace `pytest_warning_captured`, which is deprecated and will be removed in a future release. - `#6471 `_: New command-line flags: From 07ed197247bba6bf9e5f5b4323c08a26a6842880 Mon Sep 17 00:00:00 2001 From: Kostis Anagnostopoulos Date: Mon, 20 Jul 2020 15:12:48 +0300 Subject: [PATCH 489/823] doc: note about mutation of parametrized values (#7516) Fix #7514 by augmenting Note with behaviour when parametrized values are mutated (changes are reflected in subsequent test-case calls). --- doc/en/parametrize.rst | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index c8b0bf4f35a..9e531ddd45d 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -79,11 +79,21 @@ them in turn: FAILED test_expectation.py::test_eval[6*9-42] - AssertionError: assert 54... ======================= 1 failed, 2 passed in 0.12s ======================== +.. note:: + + Parameter values are passed as-is to tests (no copy whatsoever). + + For example, if you pass a list or a dict as a parameter value, and + the test case code mutates it, the mutations will be reflected in subsequent + test case calls. + .. note:: pytest by default escapes any non-ascii characters used in unicode strings for the parametrization because it has several downsides. - If however you would like to use unicode strings in parametrization and see them in the terminal as is (non-escaped), use this option in your ``pytest.ini``: + If however you would like to use unicode strings in parametrization + and see them in the terminal as is (non-escaped), use this option + in your ``pytest.ini``: .. code-block:: ini @@ -91,7 +101,8 @@ them in turn: disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True Keep in mind however that this might cause unwanted side effects and - even bugs depending on the OS used and plugins currently installed, so use it at your own risk. + even bugs depending on the OS used and plugins currently installed, + so use it at your own risk. As designed in this example, only one pair of input/output values fails From 41d211c24a6781843b174379d6d6538f5c17adb9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 20 Jul 2020 17:24:39 +0300 Subject: [PATCH 490/823] testing: use a tighter check if `bash` is available (#7520) This fixes CI on Windows since GitHub Actions started installing WSL on their images which apparently installs some wrapper `bash` which does not run actual bash. --- testing/test_parseopt.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 8cfb9e4a9cf..59b729d940f 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -1,7 +1,7 @@ import argparse import os import shlex -import shutil +import subprocess import sys import py @@ -288,8 +288,19 @@ def test_multiple_metavar_help(self, parser: parseopt.Parser) -> None: def test_argcomplete(testdir, monkeypatch) -> None: - if not shutil.which("bash"): - pytest.skip("bash not available") + try: + bash_version = subprocess.run( + ["bash", "--version"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ).stdout + except OSError: + pytest.skip("bash is not available") + if "GNU bash" not in bash_version: + # See #7518. + pytest.skip("not a real bash") + script = str(testdir.tmpdir.join("test_argcomplete")) with open(str(script), "w") as fp: From 3ed05ee4d6d58cd656a96ac9ae10cec322ea36e2 Mon Sep 17 00:00:00 2001 From: Garrett Thomas Date: Mon, 20 Jul 2020 18:16:13 +0200 Subject: [PATCH 491/823] Fix typo Change from "A xfail" to "An xfail" --- doc/en/skipping.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index c4e7d4a0a05..5c67d77a7ec 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -14,7 +14,7 @@ otherwise pytest should skip running the test altogether. Common examples are sk windows-only tests on non-windows platforms, or skipping tests that depend on an external resource which is not available at the moment (for example a database). -A **xfail** means that you expect a test to fail for some reason. +An **xfail** means that you expect a test to fail for some reason. A common example is a test for a feature not yet implemented, or a bug not yet fixed. When a test passes despite being expected to fail (marked with ``pytest.mark.xfail``), it's an **xpass** and will be reported in the test summary. From 8616a5f1d989eec5e2c5f2129040149fe4cf4347 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 20 Jul 2020 08:54:20 -0700 Subject: [PATCH 492/823] Preserve newlines when captured with capfd --- changelog/7517.bugfix.rst | 1 + src/_pytest/capture.py | 1 + testing/test_capture.py | 6 ++++++ 3 files changed, 8 insertions(+) create mode 100644 changelog/7517.bugfix.rst diff --git a/changelog/7517.bugfix.rst b/changelog/7517.bugfix.rst new file mode 100644 index 00000000000..2d062dc1e29 --- /dev/null +++ b/changelog/7517.bugfix.rst @@ -0,0 +1 @@ +Preserve line endings when captured via ``capfd``. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 3f9c60fb9a0..f538b67eceb 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -388,6 +388,7 @@ def __init__(self, targetfd: int) -> None: TemporaryFile(buffering=0), # type: ignore[arg-type] encoding="utf-8", errors="replace", + newline="", write_through=True, ) if targetfd in patchsysdict: diff --git a/testing/test_capture.py b/testing/test_capture.py index a3bd4b62327..bc89501c73b 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -514,6 +514,12 @@ def test_hello(capfd): ) reprec.assertoutcome(passed=1) + @pytest.mark.parametrize("nl", ("\n", "\r\n", "\r")) + def test_cafd_preserves_newlines(self, capfd, nl): + print("test", end=nl) + out, err = capfd.readouterr() + assert out.endswith(nl) + def test_capfdbinary(self, testdir): reprec = testdir.inline_runsource( """\ From 0709305953a7a2beff8ce78178bbfebda6bbdfdb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 21 Jul 2020 21:21:09 +0300 Subject: [PATCH 493/823] testing: improve bash check --- testing/test_parseopt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 59b729d940f..4d63d99eeab 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -292,10 +292,11 @@ def test_argcomplete(testdir, monkeypatch) -> None: bash_version = subprocess.run( ["bash", "--version"], stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=True, universal_newlines=True, ).stdout - except OSError: + except (OSError, subprocess.CalledProcessError): pytest.skip("bash is not available") if "GNU bash" not in bash_version: # See #7518. From dbc50a70610e1a2ff67b71fad2ef0813c5bce873 Mon Sep 17 00:00:00 2001 From: Kelton Bassingthwaite Date: Tue, 21 Jul 2020 20:00:47 -0600 Subject: [PATCH 494/823] Clarify usage of usefixtures mark in hooks Fix #7512 Co-authored-by: Bruno Oliveira --- changelog/7422.doc.rst | 1 + doc/en/reference.rst | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 changelog/7422.doc.rst diff --git a/changelog/7422.doc.rst b/changelog/7422.doc.rst new file mode 100644 index 00000000000..105fb4be630 --- /dev/null +++ b/changelog/7422.doc.rst @@ -0,0 +1 @@ +Clarified when the ``usefixtures`` mark can apply fixtures to test. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 005014d84d4..828a2af2776 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -180,15 +180,18 @@ pytest.mark.usefixtures Mark a test function as using the given fixture names. -.. warning:: - - This mark has no effect when applied - to a **fixture** function. - .. py:function:: pytest.mark.usefixtures(*names) :param args: the names of the fixture to use, as strings +.. note:: + + When using `usefixtures` in hooks, it can only load fixtures when applied to a test function before test setup + (for example in the `pytest_collection_modifyitems` hook). + + Also not that his mark has no effect when applied to **fixtures**. + + .. _`pytest.mark.xfail ref`: From 1a18dfd65147b4734a78b388cc4c3ec69d88600f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 22 Jul 2020 20:48:24 +0300 Subject: [PATCH 495/823] doc: mention mypy<0.750 doesn't work in typing changelog --- doc/en/changelog.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index ef5528f69c6..1107a93dd4c 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -170,9 +170,10 @@ Features noticing type errors indicating incorrect usage. If you run into an error that you believe to be incorrect, please let us know in an issue. - The types were developed against mypy version 0.780. Older versions may work, - but we recommend using at least this version. Other type checkers may work as - well, but they are not officially verified to work by pytest yet. + The types were developed against mypy version 0.780. Versions before 0.750 + are known not to work. We recommend using the latest version. Other type + checkers may work as well, but they are not officially verified to work by + pytest yet. - `#4049 `_: Introduced a new hook named `pytest_warning_recorded` to convey information about warnings captured by the internal `pytest` warnings plugin. From 7ec6401ffabf79d52938ece5b8ff566a8b9c260e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 22 Jul 2020 21:36:51 -0300 Subject: [PATCH 496/823] Change pytest deprecation warnings into errors for 6.0 release (#7362) Co-authored-by: Hugo van Kemenade --- changelog/5584.breaking.rst | 23 +++++++++++ doc/en/reference.rst | 41 +++++++++++++++++-- doc/en/warnings.rst | 31 +------------- src/_pytest/fixtures.py | 4 +- src/_pytest/hookspec.py | 5 ++- src/_pytest/mark/__init__.py | 9 ++-- src/_pytest/warning_types.py | 2 +- src/_pytest/warnings.py | 2 + src/pytest/collect.py | 5 +-- testing/acceptance_test.py | 4 +- testing/conftest.py | 4 +- testing/deprecated_test.py | 6 ++- .../fixtures/custom_item/conftest.py | 9 +++- .../conftest.py | 4 +- testing/python/collect.py | 6 +-- testing/test_collection.py | 31 ++++++++------ testing/test_junitxml.py | 13 +++--- testing/test_skipping.py | 4 +- testing/test_warnings.py | 3 -- 19 files changed, 122 insertions(+), 84 deletions(-) create mode 100644 changelog/5584.breaking.rst diff --git a/changelog/5584.breaking.rst b/changelog/5584.breaking.rst new file mode 100644 index 00000000000..990d04cb15b --- /dev/null +++ b/changelog/5584.breaking.rst @@ -0,0 +1,23 @@ +**PytestDeprecationWarning are now errors by default.** + +Following our plan to remove deprecated features with as little disruption as +possible, all warnings of type ``PytestDeprecationWarning`` now generate errors +instead of warning messages. + +**The affected features will be effectively removed in pytest 6.1**, so please consult the +`Deprecations and Removals `__ +section in the docs for directions on how to update existing code. + +In the pytest ``6.0.X`` series, it is possible to change the errors back into warnings as a +stopgap measure by adding this to your ``pytest.ini`` file: + +.. code-block:: ini + + [pytest] + filterwarnings = + ignore::pytest.PytestDeprecationWarning + +But this will stop working when pytest ``6.1`` is released. + +**If you have concerns** about the removal of a specific feature, please add a +comment to `#5584 `__. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 828a2af2776..775dd556a8a 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1024,10 +1024,45 @@ When set (regardless of value), pytest will use color in terminal output. Exceptions ---------- -UsageError -~~~~~~~~~~ - .. autoclass:: _pytest.config.UsageError() + :show-inheritance: + +.. _`warnings ref`: + +Warnings +-------- + +Custom warnings generated in some situations such as improper usage or deprecated features. + +.. autoclass:: pytest.PytestWarning + :show-inheritance: + +.. autoclass:: pytest.PytestAssertRewriteWarning + :show-inheritance: + +.. autoclass:: pytest.PytestCacheWarning + :show-inheritance: + +.. autoclass:: pytest.PytestCollectionWarning + :show-inheritance: + +.. autoclass:: pytest.PytestConfigWarning + :show-inheritance: + +.. autoclass:: pytest.PytestDeprecationWarning + :show-inheritance: + +.. autoclass:: pytest.PytestExperimentalApiWarning + :show-inheritance: + +.. autoclass:: pytest.PytestUnhandledCoroutineWarning + :show-inheritance: + +.. autoclass:: pytest.PytestUnknownMarkWarning + :show-inheritance: + + +Consult the :ref:`internal-warnings` section in the documentation for more information. .. _`ini options ref`: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 30ea529650c..d1e27ecad21 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -381,8 +381,6 @@ custom error message. Internal pytest warnings ------------------------ - - pytest may generate its own warnings in some situations, such as improper usage or deprecated features. For example, pytest will emit a warning if it encounters a class that matches :confval:`python_classes` but also @@ -415,31 +413,4 @@ These warnings might be filtered using the same builtin mechanisms used to filte Please read our :ref:`backwards-compatibility` to learn how we proceed about deprecating and eventually removing features. -The following warning types are used by pytest and are part of the public API: - -.. autoclass:: pytest.PytestWarning - :show-inheritance: - -.. autoclass:: pytest.PytestAssertRewriteWarning - :show-inheritance: - -.. autoclass:: pytest.PytestCacheWarning - :show-inheritance: - -.. autoclass:: pytest.PytestCollectionWarning - :show-inheritance: - -.. autoclass:: pytest.PytestConfigWarning - :show-inheritance: - -.. autoclass:: pytest.PytestDeprecationWarning - :show-inheritance: - -.. autoclass:: pytest.PytestExperimentalApiWarning - :show-inheritance: - -.. autoclass:: pytest.PytestUnhandledCoroutineWarning - :show-inheritance: - -.. autoclass:: pytest.PytestUnknownMarkWarning - :show-inheritance: +The full list of warnings is listed in :ref:`the reference documentation `. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b24fc5fd335..9521a7a17e8 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -46,7 +46,6 @@ from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config.argparsing import Parser -from _pytest.deprecated import FILLFUNCARGS from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS from _pytest.deprecated import FUNCARGNAMES from _pytest.mark import ParameterSet @@ -361,7 +360,8 @@ def reorder_items_atscope( def fillfixtures(function: "Function") -> None: """ fill missing funcargs for a test function. """ - warnings.warn(FILLFUNCARGS, stacklevel=2) + # Uncomment this after 6.0 release (#7361) + # warnings.warn(FILLFUNCARGS, stacklevel=2) try: request = function._request except AttributeError: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index cf3da400a8f..d21c4d4d9ef 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -12,7 +12,6 @@ from pluggy import HookspecMarker from .deprecated import COLLECT_DIRECTORY_HOOK -from .deprecated import WARNING_CAPTURED_HOOK from _pytest.compat import TYPE_CHECKING if TYPE_CHECKING: @@ -737,7 +736,9 @@ def pytest_terminal_summary( """ -@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK) +# Uncomment this after 6.0 release (#7361) +# @hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK) +@hookspec(historic=True) def pytest_warning_captured( warning_message: "warnings.WarningMessage", when: "Literal['config', 'collect', 'runtest']", diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 7bbea54d297..5d71a772526 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -1,6 +1,5 @@ """ generic mechanism for marking and selecting python functions. """ import typing -import warnings from typing import AbstractSet from typing import List from typing import Optional @@ -23,8 +22,6 @@ from _pytest.config import hookimpl from _pytest.config import UsageError from _pytest.config.argparsing import Parser -from _pytest.deprecated import MINUS_K_COLON -from _pytest.deprecated import MINUS_K_DASH from _pytest.store import StoreKey if TYPE_CHECKING: @@ -181,12 +178,14 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None: if keywordexpr.startswith("-"): # To be removed in pytest 7.0.0. - warnings.warn(MINUS_K_DASH, stacklevel=2) + # Uncomment this after 6.0 release (#7361) + # warnings.warn(MINUS_K_DASH, stacklevel=2) keywordexpr = "not " + keywordexpr[1:] selectuntil = False if keywordexpr[-1:] == ":": # To be removed in pytest 7.0.0. - warnings.warn(MINUS_K_COLON, stacklevel=2) + # Uncomment this after 6.0 release (#7361) + # warnings.warn(MINUS_K_COLON, stacklevel=2) selectuntil = True keywordexpr = keywordexpr[:-1] diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 494b92efff6..6f3b88da8b8 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -78,7 +78,7 @@ class PytestUnhandledCoroutineWarning(PytestWarning): class PytestUnknownMarkWarning(PytestWarning): """Warning emitted on use of unknown markers. - See https://docs.pytest.org/en/stable/mark.html for details. + See :ref:`mark` for details. """ __module__ = "pytest" diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 33b01b79707..3a8f2d8b33f 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -105,6 +105,8 @@ def catch_warnings_for_item( warnings.filterwarnings("always", category=DeprecationWarning) warnings.filterwarnings("always", category=PendingDeprecationWarning) + warnings.filterwarnings("error", category=pytest.PytestDeprecationWarning) + # filters should have this precedence: mark, cmdline options, ini # filters should be applied in the inverse order of precedence for arg in inifilters: diff --git a/src/pytest/collect.py b/src/pytest/collect.py index ec9c2d8b4e1..55b4b9b359c 100644 --- a/src/pytest/collect.py +++ b/src/pytest/collect.py @@ -1,11 +1,9 @@ import sys -import warnings from types import ModuleType from typing import Any from typing import List import pytest -from _pytest.deprecated import PYTEST_COLLECT_MODULE COLLECT_FAKEMODULE_ATTRIBUTES = [ @@ -33,7 +31,8 @@ def __dir__(self) -> List[str]: def __getattr__(self, name: str) -> Any: if name not in self.__all__: raise AttributeError(name) - warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2) + # Uncomment this after 6.0 release (#7361) + # warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2) return getattr(pytest, name) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index e558c7f6781..2a386e2c6e4 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -302,10 +302,10 @@ def runtest(self): pass class MyCollector(pytest.File): def collect(self): - return [MyItem(name="xyz", parent=self)] + return [MyItem.from_parent(name="xyz", parent=self)] def pytest_collect_file(path, parent): if path.basename.startswith("conftest"): - return MyCollector(path, parent) + return MyCollector.from_parent(fspath=path, parent=parent) """ ) result = testdir.runpytest(c.basename + "::" + "xyz") diff --git a/testing/conftest.py b/testing/conftest.py index f430189489e..a667be42fcb 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -116,11 +116,11 @@ def dummy_yaml_custom_test(testdir): def pytest_collect_file(parent, path): if path.ext == ".yaml" and path.basename.startswith("test"): - return YamlFile(path, parent) + return YamlFile.from_parent(fspath=path, parent=parent) class YamlFile(pytest.File): def collect(self): - yield YamlItem(self.fspath.basename, self) + yield YamlItem.from_parent(name=self.fspath.basename, parent=self) class YamlItem(pytest.Item): def runtest(self): diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 7cce092df6c..f4de3b5d9c5 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -28,6 +28,7 @@ def test(): ) +@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361") @pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore # false positive due to dynamic attribute def test_pytest_collect_module_deprecated(attribute): @@ -117,7 +118,8 @@ class MockConfig: assert w[0].filename == __file__ -def test__fillfuncargs_is_deprecated() -> None: +@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361") +def test_fillfuncargs_is_deprecated() -> None: with pytest.warns( pytest.PytestDeprecationWarning, match="The `_fillfuncargs` function is deprecated", @@ -125,6 +127,7 @@ def test__fillfuncargs_is_deprecated() -> None: pytest._fillfuncargs(mock.Mock()) +@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361") def test_minus_k_dash_is_deprecated(testdir) -> None: threepass = testdir.makepyfile( test_threepass=""" @@ -137,6 +140,7 @@ def test_three(): assert 1 result.stdout.fnmatch_lines(["*The `-k '-expr'` syntax*deprecated*"]) +@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361") def test_minus_k_colon_is_deprecated(testdir) -> None: threepass = testdir.makepyfile( test_threepass=""" diff --git a/testing/example_scripts/fixtures/custom_item/conftest.py b/testing/example_scripts/fixtures/custom_item/conftest.py index 25299d72690..161934b58f7 100644 --- a/testing/example_scripts/fixtures/custom_item/conftest.py +++ b/testing/example_scripts/fixtures/custom_item/conftest.py @@ -1,10 +1,15 @@ import pytest -class CustomItem(pytest.Item, pytest.File): +class CustomItem(pytest.Item): def runtest(self): pass +class CustomFile(pytest.File): + def collect(self): + yield CustomItem.from_parent(name="foo", parent=self) + + def pytest_collect_file(path, parent): - return CustomItem(path, parent) + return CustomFile.from_parent(fspath=path, parent=parent) diff --git a/testing/example_scripts/issue88_initial_file_multinodes/conftest.py b/testing/example_scripts/issue88_initial_file_multinodes/conftest.py index aa5d878313c..a053a638a9f 100644 --- a/testing/example_scripts/issue88_initial_file_multinodes/conftest.py +++ b/testing/example_scripts/issue88_initial_file_multinodes/conftest.py @@ -3,11 +3,11 @@ class MyFile(pytest.File): def collect(self): - return [MyItem("hello", parent=self)] + return [MyItem.from_parent(name="hello", parent=self)] def pytest_collect_file(path, parent): - return MyFile(path, parent) + return MyFile.from_parent(fspath=path, parent=parent) class MyItem(pytest.Item): diff --git a/testing/python/collect.py b/testing/python/collect.py index 80c962d7091..f64a1462971 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -762,7 +762,7 @@ class MyModule(pytest.Module): pass def pytest_pycollect_makemodule(path, parent): if path.basename == "test_xyz.py": - return MyModule(path, parent) + return MyModule.from_parent(fspath=path, parent=parent) """ ) testdir.makepyfile("def test_some(): pass") @@ -836,7 +836,7 @@ class MyFunction(pytest.Function): pass def pytest_pycollect_makeitem(collector, name, obj): if name == "some": - return MyFunction(name, collector) + return MyFunction.from_parent(name=name, parent=collector) """ ) testdir.makepyfile("def some(): pass") @@ -873,7 +873,7 @@ def find_module(self, name, path=None): def pytest_collect_file(path, parent): if path.ext == ".narf": - return Module(path, parent)""" + return Module.from_parent(fspath=path, parent=parent)""" ) testdir.makefile( ".narf", diff --git a/testing/test_collection.py b/testing/test_collection.py index 6bab509d03f..3e01e296b58 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -282,7 +282,7 @@ def test_custom_repr_failure(self, testdir): """ import pytest def pytest_collect_file(path, parent): - return MyFile(path, parent) + return MyFile.from_parent(fspath=path, parent=parent) class MyError(Exception): pass class MyFile(pytest.File): @@ -401,7 +401,7 @@ class MyModule(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": - return MyModule(path, parent) + return MyModule.from_parent(fspath=path, parent=parent) """ ) testdir.mkdir("sub") @@ -419,7 +419,7 @@ class MyModule1(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": - return MyModule1(path, parent) + return MyModule1.from_parent(fspath=path, parent=parent) """ ) conf1.move(sub1.join(conf1.basename)) @@ -430,7 +430,7 @@ class MyModule2(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": - return MyModule2(path, parent) + return MyModule2.from_parent(fspath=path, parent=parent) """ ) conf2.move(sub2.join(conf2.basename)) @@ -537,10 +537,10 @@ def runtest(self): return # ok class SpecialFile(pytest.File): def collect(self): - return [SpecialItem(name="check", parent=self)] + return [SpecialItem.from_parent(name="check", parent=self)] def pytest_collect_file(path, parent): if path.basename == %r: - return SpecialFile(fspath=path, parent=parent) + return SpecialFile.from_parent(fspath=path, parent=parent) """ % p.basename ) @@ -761,18 +761,23 @@ def pytest_configure(config): class Plugin2(object): def pytest_collect_file(self, path, parent): if path.ext == ".abc": - return MyFile2(path, parent) + return MyFile2.from_parent(fspath=path, parent=parent) def pytest_collect_file(path, parent): if path.ext == ".abc": - return MyFile1(path, parent) + return MyFile1.from_parent(fspath=path, parent=parent) + + class MyFile1(pytest.File): + def collect(self): + yield Item1.from_parent(name="item1", parent=self) - class MyFile1(pytest.Item, pytest.File): - def runtest(self): - pass class MyFile2(pytest.File): def collect(self): - return [Item2("hello", parent=self)] + yield Item2.from_parent(name="item2", parent=self) + + class Item1(pytest.Item): + def runtest(self): + pass class Item2(pytest.Item): def runtest(self): @@ -783,7 +788,7 @@ def runtest(self): result = testdir.runpytest() assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) - res = testdir.runpytest("%s::hello" % p.basename) + res = testdir.runpytest("%s::item2" % p.basename) res.stdout.fnmatch_lines(["*1 passed*"]) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index eb8475ca55c..01eeccdcd92 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -906,11 +906,8 @@ def test_summing_simple(self, testdir, run_and_parse, xunit_family): import pytest def pytest_collect_file(path, parent): if path.ext == ".xyz": - return MyItem(path, parent) + return MyItem.from_parent(name=path.basename, parent=parent) class MyItem(pytest.Item): - def __init__(self, path, parent): - super(MyItem, self).__init__(path.basename, parent) - self.fspath = path def runtest(self): raise ValueError(42) def repr_failure(self, excinfo): @@ -1336,14 +1333,14 @@ def runtest(self): class FunCollector(pytest.File): def collect(self): return [ - FunItem('a', self), - NoFunItem('a', self), - NoFunItem('b', self), + FunItem.from_parent(name='a', parent=self), + NoFunItem.from_parent(name='a', parent=self), + NoFunItem.from_parent(name='b', parent=self), ] def pytest_collect_file(path, parent): if path.check(ext='.py'): - return FunCollector(path, parent) + return FunCollector.from_parent(fspath=path, parent=parent) """ ) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 61de0b3e177..92182ff382f 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1126,7 +1126,7 @@ def runtest(self): pytest.xfail("Expected Failure") def pytest_collect_file(path, parent): - return MyItem("foo", parent) + return MyItem.from_parent(name="foo", parent=parent) """ ) result = testdir.inline_run() @@ -1206,7 +1206,7 @@ def runtest(self): assert False def pytest_collect_file(path, parent): - return MyItem("foo", parent) + return MyItem.from_parent(name="foo", parent=parent) """ ) result = testdir.inline_run() diff --git a/testing/test_warnings.py b/testing/test_warnings.py index e21ccf42ae8..c3668180216 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -520,9 +520,6 @@ def test_hidden_by_system(self, testdir, monkeypatch): @pytest.mark.parametrize("change_default", [None, "ini", "cmdline"]) -@pytest.mark.skip( - reason="This test should be enabled again before pytest 6.0 is released" -) def test_deprecation_warning_as_error(testdir, change_default): """This ensures that PytestDeprecationWarnings raised by pytest are turned into errors. From 3a060b77e81ebf990159e59cc8f8de26ad998e12 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 24 Jul 2020 21:30:38 +1000 Subject: [PATCH 497/823] Revert change to traceback repr (#7535) * Revert change to traceback repr * Add test and changelog entry * Restore *exact* prev output Co-authored-by: Bruno Oliveira --- changelog/7534.bugfix.rst | 1 + src/_pytest/_code/code.py | 10 +++++++++- testing/code/test_code.py | 10 ++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 changelog/7534.bugfix.rst diff --git a/changelog/7534.bugfix.rst b/changelog/7534.bugfix.rst new file mode 100644 index 00000000000..7c1f31360a3 --- /dev/null +++ b/changelog/7534.bugfix.rst @@ -0,0 +1 @@ +Restored the previous formatting of ``TracebackEntry.__str__`` which was changed by accident. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 218b5ad6311..219ebb68ff5 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -262,7 +262,15 @@ def __str__(self) -> str: raise except BaseException: line = "???" - return " File %r:%d in %s\n %s\n" % (self.path, self.lineno + 1, name, line) + # This output does not quite match Python's repr for traceback entries, + # but changing it to do so would break certain plugins. See + # https://github.com/pytest-dev/pytest/pull/7535/ for details. + return " File %r:%d in %s\n %s\n" % ( + str(self.path), + self.lineno + 1, + name, + line, + ) @property def name(self) -> str: diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 25a3e9aeb59..bae86be347f 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -1,3 +1,4 @@ +import re import sys from types import FrameType from unittest import mock @@ -170,6 +171,15 @@ def test_getsource(self) -> None: assert len(source) == 6 assert "assert False" in source[5] + def test_tb_entry_str(self): + try: + assert False + except AssertionError: + exci = ExceptionInfo.from_current() + pattern = r" File '.*test_code.py':\d+ in test_tb_entry_str\n assert False" + entry = str(exci.traceback[0]) + assert re.match(pattern, entry) + class TestReprFuncArgs: def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None: From c15bb5d3de084a10ade0a99e2f6ec1226aa9356a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 5 Jul 2020 22:58:47 +0300 Subject: [PATCH 498/823] pathlib: replace py.path.local.visit() with our own function Part of reducing dependency on `py`. Also enables upcoming improvements. In cases where there are simpler alternatives (in tests), I used those. What's left are a couple of uses in `_pytest.main` and `_pytest.python` and they only have modest requirements, so all of the featureful code from py is not needed. --- src/_pytest/main.py | 13 +++++-------- src/_pytest/pathlib.py | 15 +++++++++++++++ src/_pytest/python.py | 3 ++- testing/python/fixtures.py | 4 ++-- testing/test_collection.py | 5 +++-- testing/test_conftest.py | 5 +++-- 6 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 7e68165066d..7f81c341de4 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -32,6 +32,7 @@ from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit from _pytest.pathlib import Path +from _pytest.pathlib import visit from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.runner import collect_one_node @@ -617,9 +618,10 @@ def _collect( assert not names, "invalid arg {!r}".format((argpath, names)) seen_dirs = set() # type: Set[py.path.local] - for path in argpath.visit( - fil=self._visit_filter, rec=self._recurse, bf=True, sort=True - ): + for path in visit(argpath, self._recurse): + if not path.check(file=1): + continue + dirpath = path.dirpath() if dirpath not in seen_dirs: # Collect packages first. @@ -668,11 +670,6 @@ def _collect( return yield from m - @staticmethod - def _visit_filter(f: py.path.local) -> bool: - # TODO: Remove type: ignore once `py` is typed. - return f.check(file=1) # type: ignore - def _tryconvertpyarg(self, x: str) -> str: """Convert a dotted module name to path.""" try: diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 92ba32082a5..b78b13ecbd1 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -16,6 +16,7 @@ from os.path import sep from posixpath import sep as posix_sep from types import ModuleType +from typing import Callable from typing import Iterable from typing import Iterator from typing import Optional @@ -556,3 +557,17 @@ def resolve_package_path(path: Path) -> Optional[Path]: break result = parent return result + + +def visit( + path: py.path.local, recurse: Callable[[py.path.local], bool], +) -> Iterator[py.path.local]: + """Walk path recursively, in breadth-first order. + + Entries at each directory level are sorted. + """ + entries = sorted(path.listdir()) + yield from entries + for entry in entries: + if entry.check(dir=1) and recurse(entry): + yield from visit(entry, recurse) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index aa817148683..2d7060c7845 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -65,6 +65,7 @@ from _pytest.pathlib import import_path from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import parts +from _pytest.pathlib import visit from _pytest.reports import TerminalRepr from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning @@ -641,7 +642,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: ): yield Module.from_parent(self, fspath=init_module) pkg_prefixes = set() # type: Set[py.path.local] - for path in this_path.visit(rec=self._recurse, bf=True, sort=True): + for path in visit(this_path, recurse=self._recurse): # We will visit our own __init__.py file, in which case we skip it. is_file = path.isfile() if is_file: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index ca3408ece30..119c7dedaf6 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -142,14 +142,14 @@ def test_extend_fixture_conftest_module(self, testdir): p = testdir.copy_example() result = testdir.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - result = testdir.runpytest(next(p.visit("test_*.py"))) + result = testdir.runpytest(str(next(Path(str(p)).rglob("test_*.py")))) result.stdout.fnmatch_lines(["*1 passed*"]) def test_extend_fixture_conftest_conftest(self, testdir): p = testdir.copy_example() result = testdir.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - result = testdir.runpytest(next(p.visit("test_*.py"))) + result = testdir.runpytest(str(next(Path(str(p)).rglob("test_*.py")))) result.stdout.fnmatch_lines(["*1 passed*"]) def test_extend_fixture_conftest_plugin(self, testdir): diff --git a/testing/test_collection.py b/testing/test_collection.py index 3e01e296b58..f5e8abfd727 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -7,6 +7,7 @@ from _pytest.config import ExitCode from _pytest.main import _in_venv from _pytest.main import Session +from _pytest.pathlib import Path from _pytest.pathlib import symlink_or_skip from _pytest.pytester import Testdir @@ -115,8 +116,8 @@ def test_ignored_certain_directories(self, testdir): tmpdir.ensure(".whatever", "test_notfound.py") tmpdir.ensure(".bzr", "test_notfound.py") tmpdir.ensure("normal", "test_found.py") - for x in tmpdir.visit("test_*.py"): - x.write("def test_hello(): pass") + for x in Path(str(tmpdir)).rglob("test_*.py"): + x.write_text("def test_hello(): pass", "utf-8") result = testdir.runpytest("--collect-only") s = result.stdout.str() diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 724e6f464cb..dbafe7dd34b 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -477,8 +477,9 @@ def test_no_conftest(fxtr): ) ) print("created directory structure:") - for x in testdir.tmpdir.visit(): - print(" " + x.relto(testdir.tmpdir)) + tmppath = Path(str(testdir.tmpdir)) + for x in tmppath.rglob(""): + print(" " + str(x.relative_to(tmppath))) return {"runner": runner, "package": package, "swc": swc, "snc": snc} From 3633b691d88fbe9bf76137d1af25a0893ebefa84 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 5 Jul 2020 23:11:47 +0300 Subject: [PATCH 499/823] pathlib: make visit() independent of py.path.local, use os.scandir `os.scandir()`, introduced in Python 3.5, is much faster than `os.listdir()`. See https://www.python.org/dev/peps/pep-0471/. It also has a `DirEntry` which can be used to further reduce syscalls in some cases. --- src/_pytest/main.py | 6 ++++-- src/_pytest/nodes.py | 15 ++++++++------- src/_pytest/pathlib.py | 12 ++++++------ src/_pytest/python.py | 15 ++++++++------- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 7f81c341de4..96998830548 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -618,11 +618,13 @@ def _collect( assert not names, "invalid arg {!r}".format((argpath, names)) seen_dirs = set() # type: Set[py.path.local] - for path in visit(argpath, self._recurse): - if not path.check(file=1): + for direntry in visit(str(argpath), self._recurse): + if not direntry.is_file(): continue + path = py.path.local(direntry.path) dirpath = path.dirpath() + if dirpath not in seen_dirs: # Collect packages first. seen_dirs.add(dirpath) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 560548aea64..d53d591e742 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -562,17 +562,18 @@ def _gethookproxy(self, fspath: py.path.local): def gethookproxy(self, fspath: py.path.local): raise NotImplementedError() - def _recurse(self, dirpath: py.path.local) -> bool: - if dirpath.basename == "__pycache__": + def _recurse(self, direntry: "os.DirEntry[str]") -> bool: + if direntry.name == "__pycache__": return False - ihook = self._gethookproxy(dirpath.dirpath()) - if ihook.pytest_ignore_collect(path=dirpath, config=self.config): + path = py.path.local(direntry.path) + ihook = self._gethookproxy(path.dirpath()) + if ihook.pytest_ignore_collect(path=path, config=self.config): return False for pat in self._norecursepatterns: - if dirpath.check(fnmatch=pat): + if path.check(fnmatch=pat): return False - ihook = self._gethookproxy(dirpath) - ihook.pytest_collect_directory(path=dirpath, parent=self) + ihook = self._gethookproxy(path) + ihook.pytest_collect_directory(path=path, parent=self) return True def isinitpath(self, path: py.path.local) -> bool: diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index b78b13ecbd1..ba7e9948a59 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -560,14 +560,14 @@ def resolve_package_path(path: Path) -> Optional[Path]: def visit( - path: py.path.local, recurse: Callable[[py.path.local], bool], -) -> Iterator[py.path.local]: - """Walk path recursively, in breadth-first order. + path: str, recurse: Callable[["os.DirEntry[str]"], bool] +) -> Iterator["os.DirEntry[str]"]: + """Walk a directory recursively, in breadth-first order. Entries at each directory level are sorted. """ - entries = sorted(path.listdir()) + entries = sorted(os.scandir(path), key=lambda entry: entry.name) yield from entries for entry in entries: - if entry.check(dir=1) and recurse(entry): - yield from visit(entry, recurse) + if entry.is_dir(follow_symlinks=False) and recurse(entry): + yield from visit(entry.path, recurse) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2d7060c7845..e2b6aef9c44 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -642,23 +642,24 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: ): yield Module.from_parent(self, fspath=init_module) pkg_prefixes = set() # type: Set[py.path.local] - for path in visit(this_path, recurse=self._recurse): + for direntry in visit(str(this_path), recurse=self._recurse): + path = py.path.local(direntry.path) + # We will visit our own __init__.py file, in which case we skip it. - is_file = path.isfile() - if is_file: - if path.basename == "__init__.py" and path.dirpath() == this_path: + if direntry.is_file(): + if direntry.name == "__init__.py" and path.dirpath() == this_path: continue - parts_ = parts(path.strpath) + parts_ = parts(direntry.path) if any( str(pkg_prefix) in parts_ and pkg_prefix.join("__init__.py") != path for pkg_prefix in pkg_prefixes ): continue - if is_file: + if direntry.is_file(): yield from self._collectfile(path) - elif not path.isdir(): + elif not direntry.is_dir(): # Broken symlink or invalid/missing file. continue elif path.join("__init__.py").check(file=1): From 9818899df4eacb65a4ec1397a93b5ebcf43e825c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 26 Jul 2020 17:56:06 -0700 Subject: [PATCH 500/823] remove usage of pylib in docs --- .../assertion/global_testmodule_config/conftest.py | 4 ++-- doc/en/example/assertion/test_failures.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/en/example/assertion/global_testmodule_config/conftest.py b/doc/en/example/assertion/global_testmodule_config/conftest.py index da89047fe09..fd467f09e59 100644 --- a/doc/en/example/assertion/global_testmodule_config/conftest.py +++ b/doc/en/example/assertion/global_testmodule_config/conftest.py @@ -1,8 +1,8 @@ -import py +import os.path import pytest -mydir = py.path.local(__file__).dirpath() +mydir = os.path.dirname(__file__) def pytest_runtest_setup(item): diff --git a/doc/en/example/assertion/test_failures.py b/doc/en/example/assertion/test_failures.py index 30ebc72dc37..eda06dfc598 100644 --- a/doc/en/example/assertion/test_failures.py +++ b/doc/en/example/assertion/test_failures.py @@ -1,13 +1,13 @@ -import py +import os.path +import shutil -failure_demo = py.path.local(__file__).dirpath("failure_demo.py") +failure_demo = os.path.join(os.path.dirname(__file__), "failure_demo.py") pytest_plugins = ("pytester",) def test_failure_demo_fails_properly(testdir): - target = testdir.tmpdir.join(failure_demo.basename) - failure_demo.copy(target) - failure_demo.copy(testdir.tmpdir.join(failure_demo.basename)) + target = testdir.tmpdir.join(os.path.basename(failure_demo)) + shutil.copy(failure_demo, target) result = testdir.runpytest(target, syspathinsert=True) result.stdout.fnmatch_lines(["*44 failed*"]) assert result.ret != 0 From 38029828d1d6bdc15b63a873142b1e91265e1a1c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Jul 2020 08:40:14 -0300 Subject: [PATCH 501/823] Support generating major releases using issue comments (#7548) --- RELEASING.rst | 4 ++++ scripts/release-on-comment.py | 30 +++++++++++++++++------------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/RELEASING.rst b/RELEASING.rst index 9c254ea0e02..f5e2528e3f2 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -17,6 +17,10 @@ The comment must be in the form:: Where ``BRANCH`` is ``master`` or one of the maintenance branches. +For major releases the comment must be in the form:: + + @pytestbot please prepare major release from master + After that, the workflow should publish a PR and notify that it has done so as a comment in the original issue. diff --git a/scripts/release-on-comment.py b/scripts/release-on-comment.py index 521a19faca6..ae727e3dee3 100644 --- a/scripts/release-on-comment.py +++ b/scripts/release-on-comment.py @@ -2,7 +2,7 @@ This script is part of the pytest release process which is triggered by comments in issues. -This script is started by the `prepare_release.yml` workflow, which is triggered by two comment +This script is started by the `release-on-comment.yml` workflow, which is triggered by two comment related events: * https://help.github.com/en/actions/reference/events-that-trigger-workflows#issue-comment-event-issue_comment @@ -10,12 +10,13 @@ This script receives the payload and a secrets on the command line. -The payload must contain a comment with a phrase matching this regular expression: +The payload must contain a comment with a phrase matching this pseudo-regular expression: - @pytestbot please prepare release from + @pytestbot please prepare (major )? release from Then the appropriate version will be obtained based on the given branch name: +* a major release from master if "major" appears in the phrase in that position * a feature or bug fix release from master (based if there are features in the current changelog folder) * a bug fix from a maintenance branch @@ -76,15 +77,15 @@ def get_comment_data(payload: Dict) -> str: def validate_and_get_issue_comment_payload( issue_payload_path: Optional[Path], -) -> Tuple[str, str]: +) -> Tuple[str, str, bool]: payload = json.loads(issue_payload_path.read_text(encoding="UTF-8")) body = get_comment_data(payload)["body"] - m = re.match(r"@pytestbot please prepare release from ([\w\-_\.]+)", body) + m = re.match(r"@pytestbot please prepare (major )?release from ([\w\-_\.]+)", body) if m: - base_branch = m.group(1) + is_major, base_branch = m.group(1) is not None, m.group(2) else: - base_branch = None - return payload, base_branch + is_major, base_branch = False, None + return payload, base_branch, is_major def print_and_exit(msg) -> None: @@ -94,7 +95,9 @@ def print_and_exit(msg) -> None: def trigger_release(payload_path: Path, token: str) -> None: error_contents = "" # to be used to store error output in case any command fails - payload, base_branch = validate_and_get_issue_comment_payload(payload_path) + payload, base_branch, is_major = validate_and_get_issue_comment_payload( + payload_path + ) if base_branch is None: url = get_comment_data(payload)["html_url"] print_and_exit( @@ -109,10 +112,9 @@ def trigger_release(payload_path: Path, token: str) -> None: issue = repo.issue(issue_number) check_call(["git", "checkout", f"origin/{base_branch}"]) - print("DEBUG:", check_output(["git", "rev-parse", "HEAD"])) try: - version = find_next_version(base_branch) + version = find_next_version(base_branch, is_major) except InvalidFeatureRelease as e: issue.create_comment(str(e)) print_and_exit(f"{Fore.RED}{e}") @@ -215,7 +217,7 @@ def trigger_release(payload_path: Path, token: str) -> None: print_and_exit(f"{Fore.RED}{error_contents}") -def find_next_version(base_branch: str) -> str: +def find_next_version(base_branch: str, is_major: bool) -> str: output = check_output(["git", "tag"], encoding="UTF-8") valid_versions = [] for v in output.splitlines(): @@ -242,7 +244,9 @@ def find_next_version(base_branch: str) -> str: msg += "\n".join(f"* `{x.name}`" for x in sorted(features + breaking)) raise InvalidFeatureRelease(msg) - if is_feature_release: + if is_major: + return f"{last_version[0]+1}.0.0" + elif is_feature_release: return f"{last_version[0]}.{last_version[1] + 1}.0" else: return f"{last_version[0]}.{last_version[1]}.{last_version[2] + 1}" From 1e4b8d447cfaaf4ee7c4636d2a03cf484d06f1cd Mon Sep 17 00:00:00 2001 From: pytest bot Date: Tue, 28 Jul 2020 11:44:27 +0000 Subject: [PATCH 502/823] Prepare release version 6.0.0 --- changelog/5584.breaking.rst | 23 --------- changelog/7389.trivial.rst | 1 - changelog/7392.bugfix.rst | 1 - changelog/7422.doc.rst | 1 - changelog/7441.doc.rst | 1 - changelog/7464.feature.rst | 1 - changelog/7467.improvement.rst | 1 - changelog/7472.breaking.rst | 1 - changelog/7489.improvement.rst | 1 - changelog/7491.bugfix.rst | 2 - changelog/7517.bugfix.rst | 1 - changelog/7534.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-6.0.0.rst | 40 +++++++++++++++ doc/en/builtin.rst | 13 +++-- doc/en/changelog.rst | 85 +++++++++++++++++++++++++++++++ doc/en/example/parametrize.rst | 4 +- doc/en/getting-started.rst | 2 +- doc/en/writing_plugins.rst | 7 +-- 19 files changed, 138 insertions(+), 49 deletions(-) delete mode 100644 changelog/5584.breaking.rst delete mode 100644 changelog/7389.trivial.rst delete mode 100644 changelog/7392.bugfix.rst delete mode 100644 changelog/7422.doc.rst delete mode 100644 changelog/7441.doc.rst delete mode 100644 changelog/7464.feature.rst delete mode 100644 changelog/7467.improvement.rst delete mode 100644 changelog/7472.breaking.rst delete mode 100644 changelog/7489.improvement.rst delete mode 100644 changelog/7491.bugfix.rst delete mode 100644 changelog/7517.bugfix.rst delete mode 100644 changelog/7534.bugfix.rst create mode 100644 doc/en/announce/release-6.0.0.rst diff --git a/changelog/5584.breaking.rst b/changelog/5584.breaking.rst deleted file mode 100644 index 990d04cb15b..00000000000 --- a/changelog/5584.breaking.rst +++ /dev/null @@ -1,23 +0,0 @@ -**PytestDeprecationWarning are now errors by default.** - -Following our plan to remove deprecated features with as little disruption as -possible, all warnings of type ``PytestDeprecationWarning`` now generate errors -instead of warning messages. - -**The affected features will be effectively removed in pytest 6.1**, so please consult the -`Deprecations and Removals `__ -section in the docs for directions on how to update existing code. - -In the pytest ``6.0.X`` series, it is possible to change the errors back into warnings as a -stopgap measure by adding this to your ``pytest.ini`` file: - -.. code-block:: ini - - [pytest] - filterwarnings = - ignore::pytest.PytestDeprecationWarning - -But this will stop working when pytest ``6.1`` is released. - -**If you have concerns** about the removal of a specific feature, please add a -comment to `#5584 `__. diff --git a/changelog/7389.trivial.rst b/changelog/7389.trivial.rst deleted file mode 100644 index 00cfe92bcee..00000000000 --- a/changelog/7389.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Fixture scope ``package`` is no longer considered experimental. diff --git a/changelog/7392.bugfix.rst b/changelog/7392.bugfix.rst deleted file mode 100644 index 48cd949faef..00000000000 --- a/changelog/7392.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix the reported location of tests skipped with ``@pytest.mark.skip`` when ``--runxfail`` is used. diff --git a/changelog/7422.doc.rst b/changelog/7422.doc.rst deleted file mode 100644 index 105fb4be630..00000000000 --- a/changelog/7422.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Clarified when the ``usefixtures`` mark can apply fixtures to test. diff --git a/changelog/7441.doc.rst b/changelog/7441.doc.rst deleted file mode 100644 index d7c770e995d..00000000000 --- a/changelog/7441.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add a note about ``-q`` option used in getting started guide. diff --git a/changelog/7464.feature.rst b/changelog/7464.feature.rst deleted file mode 100644 index 8b27ee96429..00000000000 --- a/changelog/7464.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added support for :envvar:`NO_COLOR` and :envvar:`FORCE_COLOR` environment variables to control colored output. diff --git a/changelog/7467.improvement.rst b/changelog/7467.improvement.rst deleted file mode 100644 index b7cf3b4d8aa..00000000000 --- a/changelog/7467.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``--log-file`` CLI option and ``log_file`` ini marker now create subdirectories if needed. diff --git a/changelog/7472.breaking.rst b/changelog/7472.breaking.rst deleted file mode 100644 index b76874e3779..00000000000 --- a/changelog/7472.breaking.rst +++ /dev/null @@ -1 +0,0 @@ -The ``exec_()`` and ``is_true()`` methods of ``_pytest._code.Frame`` have been removed. diff --git a/changelog/7489.improvement.rst b/changelog/7489.improvement.rst deleted file mode 100644 index 218342f2d29..00000000000 --- a/changelog/7489.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -The :func:`pytest.raises` function has a clearer error message when ``match`` equals the obtained string but is not a regex match. In this case it is suggested to escape the regex. diff --git a/changelog/7491.bugfix.rst b/changelog/7491.bugfix.rst deleted file mode 100644 index 5b00a5713b5..00000000000 --- a/changelog/7491.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -:fixture:`tmpdir` and :fixture:`tmp_path` no longer raise an error if the lock to check for -stale temporary directories is not accessible. diff --git a/changelog/7517.bugfix.rst b/changelog/7517.bugfix.rst deleted file mode 100644 index 2d062dc1e29..00000000000 --- a/changelog/7517.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Preserve line endings when captured via ``capfd``. diff --git a/changelog/7534.bugfix.rst b/changelog/7534.bugfix.rst deleted file mode 100644 index 7c1f31360a3..00000000000 --- a/changelog/7534.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Restored the previous formatting of ``TracebackEntry.__str__`` which was changed by accident. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 31deaad71c8..7d176aa062c 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.0.0 release-6.0.0rc1 release-5.4.3 release-5.4.2 diff --git a/doc/en/announce/release-6.0.0.rst b/doc/en/announce/release-6.0.0.rst new file mode 100644 index 00000000000..a4b0571e479 --- /dev/null +++ b/doc/en/announce/release-6.0.0.rst @@ -0,0 +1,40 @@ +pytest-6.0.0 +======================================= + +The pytest team is proud to announce the 6.0.0 release! + +pytest is a mature Python testing tool with more than a 2000 tests +against itself, passing on many different interpreters and platforms. + +This release contains a number of bug fixes and improvements, so users are encouraged +to take a look at the CHANGELOG: + + https://docs.pytest.org/en/latest/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/latest/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Arvin Firouzi +* Bruno Oliveira +* Debi Mishra +* Garrett Thomas +* Hugo van Kemenade +* Kelton Bassingthwaite +* Kostis Anagnostopoulos +* Lewis Cowles +* Miro Hrončok +* Ran Benita +* Simon K +* Zac Hatfield-Dodds + + +Happy testing, +The pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 7864233fc81..b33ee041dd6 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -69,11 +69,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a ... record_property - Add an extra properties the calling test. + Add extra properties to the calling test. + User properties become part of the test report and are available to the configured reporters, like JUnit XML. - The fixture is callable with ``(name, value)``, with value being automatically - xml-encoded. + + The fixture is callable with ``name, value``. The value is automatically + XML-encoded. Example:: @@ -82,8 +84,9 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a record_xml_attribute Add extra xml attributes to the tag for the calling test. - The fixture is callable with ``(name, value)``, with value being - automatically xml-encoded + + The fixture is callable with ``name, value``. The value is + automatically XML-encoded. record_testsuite_property [session scope] Records a new ```` tag as child of the root ````. This is suitable to diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 859dfea7975..2b515538a0b 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,91 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 6.0.0 (2020-07-28) +========================= + +Breaking Changes +---------------- + +- `#5584 `_: **PytestDeprecationWarning are now errors by default.** + + Following our plan to remove deprecated features with as little disruption as + possible, all warnings of type ``PytestDeprecationWarning`` now generate errors + instead of warning messages. + + **The affected features will be effectively removed in pytest 6.1**, so please consult the + `Deprecations and Removals `__ + section in the docs for directions on how to update existing code. + + In the pytest ``6.0.X`` series, it is possible to change the errors back into warnings as a + stopgap measure by adding this to your ``pytest.ini`` file: + + .. code-block:: ini + + [pytest] + filterwarnings = + ignore::pytest.PytestDeprecationWarning + + But this will stop working when pytest ``6.1`` is released. + + **If you have concerns** about the removal of a specific feature, please add a + comment to `#5584 `__. + + +- `#7472 `_: The ``exec_()`` and ``is_true()`` methods of ``_pytest._code.Frame`` have been removed. + + + +Features +-------- + +- `#7464 `_: Added support for :envvar:`NO_COLOR` and :envvar:`FORCE_COLOR` environment variables to control colored output. + + + +Improvements +------------ + +- `#7467 `_: ``--log-file`` CLI option and ``log_file`` ini marker now create subdirectories if needed. + + +- `#7489 `_: The :func:`pytest.raises` function has a clearer error message when ``match`` equals the obtained string but is not a regex match. In this case it is suggested to escape the regex. + + + +Bug Fixes +--------- + +- `#7392 `_: Fix the reported location of tests skipped with ``@pytest.mark.skip`` when ``--runxfail`` is used. + + +- `#7491 `_: :fixture:`tmpdir` and :fixture:`tmp_path` no longer raise an error if the lock to check for + stale temporary directories is not accessible. + + +- `#7517 `_: Preserve line endings when captured via ``capfd``. + + +- `#7534 `_: Restored the previous formatting of ``TracebackEntry.__str__`` which was changed by accident. + + + +Improved Documentation +---------------------- + +- `#7422 `_: Clarified when the ``usefixtures`` mark can apply fixtures to test. + + +- `#7441 `_: Add a note about ``-q`` option used in getting started guide. + + + +Trivial/Internal Changes +------------------------ + +- `#7389 `_: Fixture scope ``package`` is no longer considered experimental. + + pytest 6.0.0rc1 (2020-07-08) ============================ diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 8fa48bfe340..f1c98d449fb 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -508,10 +508,10 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ssssssssssssssssssssssss... [100%] + ssssssssssss...ssssssssssss [100%] ========================= short test summary info ========================== SKIPPED [12] multipython.py:29: 'python3.5' not found - SKIPPED [12] multipython.py:29: 'python3.6' not found + SKIPPED [12] multipython.py:29: 'python3.7' not found 3 passed, 24 skipped in 0.12s Indirect parametrization of optional implementations/imports diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 38adf68e0bd..a2f6daa392a 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 6.0.0rc1 + pytest 6.0.0 .. _`simpletest`: diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 27ef40e5b39..cf4dbf99fea 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -442,13 +442,8 @@ additionally it is possible to copy examples for an example folder before runnin $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time testdir.copy_example("test_example.py") - test_example.py::test_plugin - $PYTHON_PREFIX/lib/python3.7/site-packages/_pytest/compat.py:340: PytestDeprecationWarning: The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk. - See https://docs.pytest.org/en/stable/deprecations.html#terminalreporter-writer for more information. - return getattr(object, name, default) - -- Docs: https://docs.pytest.org/en/stable/warnings.html - ====================== 2 passed, 2 warnings in 0.12s ======================= + ======================= 2 passed, 1 warning in 0.12s ======================= For more information about the result object that ``runpytest()`` returns, and the methods that it provides please check out the :py:class:`RunResult From 45ced1dc056d586fe3714823fa033cab27055c9f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Jul 2020 10:46:03 -0300 Subject: [PATCH 503/823] Update doc/en/announce/release-6.0.0.rst Co-authored-by: Hugo van Kemenade --- doc/en/announce/release-6.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/announce/release-6.0.0.rst b/doc/en/announce/release-6.0.0.rst index a4b0571e479..9706fe59bc7 100644 --- a/doc/en/announce/release-6.0.0.rst +++ b/doc/en/announce/release-6.0.0.rst @@ -3,7 +3,7 @@ pytest-6.0.0 The pytest team is proud to announce the 6.0.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bug fixes and improvements, so users are encouraged From 41a453959441d9b03cba3e47730efca27fa2f252 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Jul 2020 14:15:45 -0300 Subject: [PATCH 504/823] Add link to 6.0.0rc1 changelog --- doc/en/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 2b515538a0b..2ad8de2124a 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -31,6 +31,8 @@ with advance notice in the **Deprecations** section of releases. pytest 6.0.0 (2020-07-28) ========================= +(**Please see the full set of changes for this release also in the 6.0.0rc1 notes below**) + Breaking Changes ---------------- From 70764bef4f0915c192213fc8c080a50221679982 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Jul 2020 17:01:27 -0300 Subject: [PATCH 505/823] Merge pull request #7550 from pytest-dev/release-6.0.0 --- doc/en/announce/index.rst | 1 + doc/en/announce/release-6.0.0.rst | 40 ++++++++++++++ doc/en/builtin.rst | 13 +++-- doc/en/changelog.rst | 87 +++++++++++++++++++++++++++++++ doc/en/example/parametrize.rst | 4 +- doc/en/getting-started.rst | 2 +- doc/en/writing_plugins.rst | 7 +-- 7 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 doc/en/announce/release-6.0.0.rst diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 31deaad71c8..7d176aa062c 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.0.0 release-6.0.0rc1 release-5.4.3 release-5.4.2 diff --git a/doc/en/announce/release-6.0.0.rst b/doc/en/announce/release-6.0.0.rst new file mode 100644 index 00000000000..9706fe59bc7 --- /dev/null +++ b/doc/en/announce/release-6.0.0.rst @@ -0,0 +1,40 @@ +pytest-6.0.0 +======================================= + +The pytest team is proud to announce the 6.0.0 release! + +pytest is a mature Python testing tool with more than 2000 tests +against itself, passing on many different interpreters and platforms. + +This release contains a number of bug fixes and improvements, so users are encouraged +to take a look at the CHANGELOG: + + https://docs.pytest.org/en/latest/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/latest/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Arvin Firouzi +* Bruno Oliveira +* Debi Mishra +* Garrett Thomas +* Hugo van Kemenade +* Kelton Bassingthwaite +* Kostis Anagnostopoulos +* Lewis Cowles +* Miro Hrončok +* Ran Benita +* Simon K +* Zac Hatfield-Dodds + + +Happy testing, +The pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 7864233fc81..b33ee041dd6 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -69,11 +69,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a ... record_property - Add an extra properties the calling test. + Add extra properties to the calling test. + User properties become part of the test report and are available to the configured reporters, like JUnit XML. - The fixture is callable with ``(name, value)``, with value being automatically - xml-encoded. + + The fixture is callable with ``name, value``. The value is automatically + XML-encoded. Example:: @@ -82,8 +84,9 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a record_xml_attribute Add extra xml attributes to the tag for the calling test. - The fixture is callable with ``(name, value)``, with value being - automatically xml-encoded + + The fixture is callable with ``name, value``. The value is + automatically XML-encoded. record_testsuite_property [session scope] Records a new ```` tag as child of the root ````. This is suitable to diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 859dfea7975..2ad8de2124a 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,93 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 6.0.0 (2020-07-28) +========================= + +(**Please see the full set of changes for this release also in the 6.0.0rc1 notes below**) + +Breaking Changes +---------------- + +- `#5584 `_: **PytestDeprecationWarning are now errors by default.** + + Following our plan to remove deprecated features with as little disruption as + possible, all warnings of type ``PytestDeprecationWarning`` now generate errors + instead of warning messages. + + **The affected features will be effectively removed in pytest 6.1**, so please consult the + `Deprecations and Removals `__ + section in the docs for directions on how to update existing code. + + In the pytest ``6.0.X`` series, it is possible to change the errors back into warnings as a + stopgap measure by adding this to your ``pytest.ini`` file: + + .. code-block:: ini + + [pytest] + filterwarnings = + ignore::pytest.PytestDeprecationWarning + + But this will stop working when pytest ``6.1`` is released. + + **If you have concerns** about the removal of a specific feature, please add a + comment to `#5584 `__. + + +- `#7472 `_: The ``exec_()`` and ``is_true()`` methods of ``_pytest._code.Frame`` have been removed. + + + +Features +-------- + +- `#7464 `_: Added support for :envvar:`NO_COLOR` and :envvar:`FORCE_COLOR` environment variables to control colored output. + + + +Improvements +------------ + +- `#7467 `_: ``--log-file`` CLI option and ``log_file`` ini marker now create subdirectories if needed. + + +- `#7489 `_: The :func:`pytest.raises` function has a clearer error message when ``match`` equals the obtained string but is not a regex match. In this case it is suggested to escape the regex. + + + +Bug Fixes +--------- + +- `#7392 `_: Fix the reported location of tests skipped with ``@pytest.mark.skip`` when ``--runxfail`` is used. + + +- `#7491 `_: :fixture:`tmpdir` and :fixture:`tmp_path` no longer raise an error if the lock to check for + stale temporary directories is not accessible. + + +- `#7517 `_: Preserve line endings when captured via ``capfd``. + + +- `#7534 `_: Restored the previous formatting of ``TracebackEntry.__str__`` which was changed by accident. + + + +Improved Documentation +---------------------- + +- `#7422 `_: Clarified when the ``usefixtures`` mark can apply fixtures to test. + + +- `#7441 `_: Add a note about ``-q`` option used in getting started guide. + + + +Trivial/Internal Changes +------------------------ + +- `#7389 `_: Fixture scope ``package`` is no longer considered experimental. + + pytest 6.0.0rc1 (2020-07-08) ============================ diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 8fa48bfe340..f1c98d449fb 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -508,10 +508,10 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ssssssssssssssssssssssss... [100%] + ssssssssssss...ssssssssssss [100%] ========================= short test summary info ========================== SKIPPED [12] multipython.py:29: 'python3.5' not found - SKIPPED [12] multipython.py:29: 'python3.6' not found + SKIPPED [12] multipython.py:29: 'python3.7' not found 3 passed, 24 skipped in 0.12s Indirect parametrization of optional implementations/imports diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 38adf68e0bd..a2f6daa392a 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 6.0.0rc1 + pytest 6.0.0 .. _`simpletest`: diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 27ef40e5b39..cf4dbf99fea 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -442,13 +442,8 @@ additionally it is possible to copy examples for an example folder before runnin $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time testdir.copy_example("test_example.py") - test_example.py::test_plugin - $PYTHON_PREFIX/lib/python3.7/site-packages/_pytest/compat.py:340: PytestDeprecationWarning: The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk. - See https://docs.pytest.org/en/stable/deprecations.html#terminalreporter-writer for more information. - return getattr(object, name, default) - -- Docs: https://docs.pytest.org/en/stable/warnings.html - ====================== 2 passed, 2 warnings in 0.12s ======================= + ======================= 2 passed, 1 warning in 0.12s ======================= For more information about the result object that ``runpytest()`` returns, and the methods that it provides please check out the :py:class:`RunResult From 109b6cb32c9e9d4fa2d9e36c828178fee04a4974 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Jul 2020 17:14:48 -0300 Subject: [PATCH 506/823] Update text and links in announce templates The links were still pointing to the latest version (instead of stable) and also took the opportunity to update the text a bit. --- scripts/release.minor.rst | 13 +++++-------- scripts/release.patch.rst | 4 ++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/scripts/release.minor.rst b/scripts/release.minor.rst index f71f9b1b64c..76e447f0c6d 100644 --- a/scripts/release.minor.rst +++ b/scripts/release.minor.rst @@ -3,23 +3,20 @@ pytest-{version} The pytest team is proud to announce the {version} release! -pytest is a mature Python testing tool with more than a 2000 tests -against itself, passing on many different interpreters and platforms. +This release contains new features, improvements, bug fixes, and breaking changes, so users +are encouraged to take a look at the CHANGELOG carefully: -This release contains a number of bug fixes and improvements, so users are encouraged -to take a look at the CHANGELOG: - - https://docs.pytest.org/en/latest/changelog.html + https://docs.pytest.org/en/stable/changelog.html For complete documentation, please visit: - https://docs.pytest.org/en/latest/ + https://docs.pytest.org/en/stable/ As usual, you can upgrade from PyPI via: pip install -U pytest -Thanks to all who contributed to this release, among them: +Thanks to all of the contributors to this release: {contributors} diff --git a/scripts/release.patch.rst b/scripts/release.patch.rst index b1ad2dbd775..59fbe50ce0e 100644 --- a/scripts/release.patch.rst +++ b/scripts/release.patch.rst @@ -7,9 +7,9 @@ This is a bug-fix release, being a drop-in replacement. To upgrade:: pip install --upgrade pytest -The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. -Thanks to all who contributed to this release, among them: +Thanks to all of the contributors to this release: {contributors} From 88cc636c181f7283acb19b2e6154dca187124b8f Mon Sep 17 00:00:00 2001 From: Drew Devereux Date: Wed, 29 Jul 2020 15:10:13 +0800 Subject: [PATCH 507/823] Update markers.rst (#7563) Extra colon to make code block render correctly --- doc/en/example/markers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 38610ee3a60..99e3386b8b5 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -280,7 +280,7 @@ its test methods: This is equivalent to directly applying the decorator to the two test functions. -To apply marks at the module level, use the :globalvar:`pytestmark` global variable: +To apply marks at the module level, use the :globalvar:`pytestmark` global variable:: import pytest pytestmark = pytest.mark.webtest From b36bcd13e9dd054ce320ab204e5842b2b379e130 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Jul 2020 10:34:06 +0300 Subject: [PATCH 508/823] mark: fix pylint not-callable error on pytest.mark.parametrize(...), again Apparently the previous fix c1ca42b5c264b4ea1b4591bc12e0 didn't work. Hopefully this time I'm testing this correctly. --- changelog/7558.bugfix.rst | 2 ++ src/_pytest/mark/structures.py | 14 ++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 changelog/7558.bugfix.rst diff --git a/changelog/7558.bugfix.rst b/changelog/7558.bugfix.rst new file mode 100644 index 00000000000..6e3ec674c9b --- /dev/null +++ b/changelog/7558.bugfix.rst @@ -0,0 +1,2 @@ +Fix pylint ``not-callable`` lint on ``pytest.mark.parametrize()`` and the other builtin marks: +``skip``, ``skipif``, ``xfail``, ``usefixtures``, ``filterwarnings``. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 5edeecdd5a4..9f8ce4ebce3 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -4,7 +4,6 @@ import warnings from typing import Any from typing import Callable -from typing import cast from typing import Iterable from typing import List from typing import Mapping @@ -473,14 +472,13 @@ def test_function(): # See TYPE_CHECKING above. if TYPE_CHECKING: - # Using casts instead of type comments intentionally - issue #7473. # TODO(py36): Change to builtin annotation syntax. - skip = cast(_SkipMarkDecorator, None) - skipif = cast(_SkipifMarkDecorator, None) - xfail = cast(_XfailMarkDecorator, None) - parametrize = cast(_ParametrizeMarkDecorator, None) - usefixtures = cast(_UsefixturesMarkDecorator, None) - filterwarnings = cast(_FilterwarningsMarkDecorator, None) + skip = _SkipMarkDecorator(Mark("skip", (), {})) + skipif = _SkipifMarkDecorator(Mark("skipif", (), {})) + xfail = _XfailMarkDecorator(Mark("xfail", (), {})) + parametrize = _ParametrizeMarkDecorator(Mark("parametrize ", (), {})) + usefixtures = _UsefixturesMarkDecorator(Mark("usefixtures ", (), {})) + filterwarnings = _FilterwarningsMarkDecorator(Mark("filterwarnings ", (), {})) def __getattr__(self, name: str) -> MarkDecorator: if name[0] == "_": From 27a4c6cd6dffef163d4593338f63926fd5a4d068 Mon Sep 17 00:00:00 2001 From: hp310780 <43389959+hp310780@users.noreply.github.com> Date: Wed, 29 Jul 2020 08:48:38 +0100 Subject: [PATCH 509/823] Fix --help crash on add_ini(.., help='') and improve message on help=None (#7427) --- changelog/7394.bugfix.rst | 2 ++ src/_pytest/helpconfig.py | 9 ++++++--- testing/test_helpconfig.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 changelog/7394.bugfix.rst diff --git a/changelog/7394.bugfix.rst b/changelog/7394.bugfix.rst new file mode 100644 index 00000000000..b39558cf1d2 --- /dev/null +++ b/changelog/7394.bugfix.rst @@ -0,0 +1,2 @@ +Passing an empty ``help`` value to ``Parser.add_option`` is now accepted instead of crashing when running ``pytest --help``. +Passing ``None`` raises a more informative ``TypeError``. diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 24952852be4..f3623b8a103 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -170,6 +170,8 @@ def showhelp(config: Config) -> None: help, type, default = config._parser._inidict[name] if type is None: type = "string" + if help is None: + raise TypeError("help argument cannot be None for {}".format(name)) spec = "{} ({}):".format(name, type) tw.write(" %s" % spec) spec_len = len(spec) @@ -191,9 +193,10 @@ def showhelp(config: Config) -> None: tw.write(" " * (indent_len - spec_len - 2)) wrapped = textwrap.wrap(help, columns - indent_len, break_on_hyphens=False) - tw.line(wrapped[0]) - for line in wrapped[1:]: - tw.line(indent + line) + if wrapped: + tw.line(wrapped[0]) + for line in wrapped[1:]: + tw.line(indent + line) tw.line() tw.line("environment variables:") diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index 24590dd3b55..a33273a2c1d 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -38,6 +38,41 @@ def test_help(testdir): ) +def test_none_help_param_raises_exception(testdir): + """Tests a None help param raises a TypeError. + """ + testdir.makeconftest( + """ + def pytest_addoption(parser): + parser.addini("test_ini", None, default=True, type="bool") + """ + ) + result = testdir.runpytest("--help") + result.stderr.fnmatch_lines( + ["*TypeError: help argument cannot be None for test_ini*"] + ) + + +def test_empty_help_param(testdir): + """Tests an empty help param is displayed correctly. + """ + testdir.makeconftest( + """ + def pytest_addoption(parser): + parser.addini("test_ini", "", default=True, type="bool") + """ + ) + result = testdir.runpytest("--help") + assert result.ret == 0 + lines = [ + " required_plugins (args):", + " plugins that must be present for pytest to run*", + " test_ini (bool):*", + "environment variables:", + ] + result.stdout.fnmatch_lines(lines, consecutive=True) + + def test_hookvalidation_unknown(testdir): testdir.makeconftest( """ From 6ea6f0dac80b9188536131201098f2c12a9b9987 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 23 Jul 2020 17:33:17 +0300 Subject: [PATCH 510/823] junitxml: compile a regex lazily Instead of slowing down startup, and making the code harder to follow, compile it lazily (it is still cached internally). --- src/_pytest/junitxml.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 8c68d196a2c..6f0d903308f 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -68,8 +68,6 @@ class Junit(py.xml.Namespace): del _legal_ranges del _legal_xml_re -_py_ext_re = re.compile(r"\.py$") - def bin_xml_escape(arg: object) -> py.xml.raw: def repl(matchobj: Match[str]) -> str: @@ -473,7 +471,7 @@ def mangle_test_address(address: str) -> List[str]: pass # convert file path to dotted path names[0] = names[0].replace(nodes.SEP, ".") - names[0] = _py_ext_re.sub("", names[0]) + names[0] = re.sub(r"\.py$", "", names[0]) # put any params back names[-1] += possible_open_bracket + params return names From 1653c49b1b85271012a278fb923c98ebb8245575 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 23 Jul 2020 17:37:05 +0300 Subject: [PATCH 511/823] junitxml: simplify bin_xml_escape 1. Remove sys.maxunicode check & comment. Nowadays it is always a constant 0x10ffff. 2. Pre-generate the pattern. Possible due to 1. 3. Compile the regex lazily. No reason to pay startup cost for it. 4. Add docstring in particular to explain a subtle point. --- src/_pytest/junitxml.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 6f0d903308f..dff39d1ba57 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -12,7 +12,6 @@ import os import platform import re -import sys from datetime import datetime from typing import Callable from typing import Dict @@ -50,26 +49,20 @@ class Junit(py.xml.Namespace): pass -# We need to get the subset of the invalid unicode ranges according to -# XML 1.0 which are valid in this python build. Hence we calculate -# this dynamically instead of hardcoding it. The spec range of valid -# chars is: Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] -# | [#x10000-#x10FFFF] -_legal_chars = (0x09, 0x0A, 0x0D) -_legal_ranges = ((0x20, 0x7E), (0x80, 0xD7FF), (0xE000, 0xFFFD), (0x10000, 0x10FFFF)) -_legal_xml_re = [ - "{}-{}".format(chr(low), chr(high)) - for (low, high) in _legal_ranges - if low < sys.maxunicode -] -_legal_xml_re = [chr(x) for x in _legal_chars] + _legal_xml_re -illegal_xml_re = re.compile("[^%s]" % "".join(_legal_xml_re)) -del _legal_chars -del _legal_ranges -del _legal_xml_re +def bin_xml_escape(arg: object) -> py.xml.raw: + r"""Visually escape an object into valid a XML string. + For example, transforms + 'hello\aworld\b' + into + 'hello#x07world#x08' + Note that the #xABs are *not* XML escapes - missing the ampersand «. + The idea is to escape visually for the user rather than for XML itself. + + The result is also entity-escaped and wrapped in py.xml.raw() so it can + be embedded directly. + """ -def bin_xml_escape(arg: object) -> py.xml.raw: def repl(matchobj: Match[str]) -> str: i = ord(matchobj.group()) if i <= 0xFF: @@ -77,7 +70,13 @@ def repl(matchobj: Match[str]) -> str: else: return "#x%04X" % i - return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(str(arg)))) + # The spec range of valid chars is: + # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + # For an unknown(?) reason, we disallow #x7F (DEL) as well. + illegal_xml_re = ( + "[^\u0009\u000A\u000D\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]" + ) + return py.xml.raw(re.sub(illegal_xml_re, repl, py.xml.escape(str(arg)))) def merge_family(left, right) -> None: From f86e4516eb6cbb5ba5c2d294359882674a215935 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 23 Jul 2020 15:31:14 +0300 Subject: [PATCH 512/823] junitxml: convert from py.xml to xml.etree.ElementTree Part of the effort to reduce dependency on the py library. Besides that, py.xml implements its own XML serialization which is pretty scary. I tried to keep the code with minimal changes (though it could use some cleanups). The differences in behavior I have noticed are: - Attributes in the output are not sorted. - Some unneeded escaping is no longer performed, for example escaping `"` to `"` in a text node. --- changelog/7536.trivial.rst | 3 + src/_pytest/junitxml.py | 136 ++++++++++++++++--------------------- testing/test_junitxml.py | 11 +-- 3 files changed, 68 insertions(+), 82 deletions(-) create mode 100644 changelog/7536.trivial.rst diff --git a/changelog/7536.trivial.rst b/changelog/7536.trivial.rst new file mode 100644 index 00000000000..f713da4f1a0 --- /dev/null +++ b/changelog/7536.trivial.rst @@ -0,0 +1,3 @@ +The internal ``junitxml`` plugin has rewritten to use ``xml.etree.ElementTree``. +The order of attributes in XML elements might differ. Some unneeded escaping is +no longer performed. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index dff39d1ba57..28ae69e82ac 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -12,6 +12,7 @@ import os import platform import re +import xml.etree.ElementTree as ET from datetime import datetime from typing import Callable from typing import Dict @@ -21,14 +22,11 @@ from typing import Tuple from typing import Union -import py - import pytest from _pytest import deprecated from _pytest import nodes from _pytest import timing from _pytest._code.code import ExceptionRepr -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import filename_arg from _pytest.config.argparsing import Parser @@ -38,19 +36,12 @@ from _pytest.terminal import TerminalReporter from _pytest.warnings import _issue_warning_captured -if TYPE_CHECKING: - from typing import Type - xml_key = StoreKey["LogXML"]() -class Junit(py.xml.Namespace): - pass - - -def bin_xml_escape(arg: object) -> py.xml.raw: - r"""Visually escape an object into valid a XML string. +def bin_xml_escape(arg: object) -> str: + r"""Visually escape invalid XML characters. For example, transforms 'hello\aworld\b' @@ -58,9 +49,6 @@ def bin_xml_escape(arg: object) -> py.xml.raw: 'hello#x07world#x08' Note that the #xABs are *not* XML escapes - missing the ampersand «. The idea is to escape visually for the user rather than for XML itself. - - The result is also entity-escaped and wrapped in py.xml.raw() so it can - be embedded directly. """ def repl(matchobj: Match[str]) -> str: @@ -76,7 +64,7 @@ def repl(matchobj: Match[str]) -> str: illegal_xml_re = ( "[^\u0009\u000A\u000D\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]" ) - return py.xml.raw(re.sub(illegal_xml_re, repl, py.xml.escape(str(arg)))) + return re.sub(illegal_xml_re, repl, str(arg)) def merge_family(left, right) -> None: @@ -108,12 +96,12 @@ def __init__(self, nodeid: Union[str, TestReport], xml: "LogXML") -> None: self.add_stats = self.xml.add_stats self.family = self.xml.family self.duration = 0 - self.properties = [] # type: List[Tuple[str, py.xml.raw]] - self.nodes = [] # type: List[py.xml.Tag] - self.attrs = {} # type: Dict[str, Union[str, py.xml.raw]] + self.properties = [] # type: List[Tuple[str, str]] + self.nodes = [] # type: List[ET.Element] + self.attrs = {} # type: Dict[str, str] - def append(self, node: py.xml.Tag) -> None: - self.xml.add_stats(type(node).__name__) + def append(self, node: ET.Element) -> None: + self.xml.add_stats(node.tag) self.nodes.append(node) def add_property(self, name: str, value: object) -> None: @@ -122,17 +110,15 @@ def add_property(self, name: str, value: object) -> None: def add_attribute(self, name: str, value: object) -> None: self.attrs[str(name)] = bin_xml_escape(value) - def make_properties_node(self) -> Union[py.xml.Tag, str]: + def make_properties_node(self) -> Optional[ET.Element]: """Return a Junit node containing custom properties, if any. """ if self.properties: - return Junit.properties( - [ - Junit.property(name=name, value=value) - for name, value in self.properties - ] - ) - return "" + properties = ET.Element("properties") + for name, value in self.properties: + properties.append(ET.Element("property", name=name, value=value)) + return properties + return None def record_testreport(self, testreport: TestReport) -> None: names = mangle_test_address(testreport.nodeid) @@ -144,7 +130,7 @@ def record_testreport(self, testreport: TestReport) -> None: "classname": ".".join(classnames), "name": bin_xml_escape(names[-1]), "file": testreport.location[0], - } # type: Dict[str, Union[str, py.xml.raw]] + } # type: Dict[str, str] if testreport.location[1] is not None: attrs["line"] = str(testreport.location[1]) if hasattr(testreport, "url"): @@ -164,16 +150,17 @@ def record_testreport(self, testreport: TestReport) -> None: temp_attrs[key] = self.attrs[key] self.attrs = temp_attrs - def to_xml(self) -> py.xml.Tag: - testcase = Junit.testcase(time="%.3f" % self.duration, **self.attrs) - testcase.append(self.make_properties_node()) - for node in self.nodes: - testcase.append(node) + def to_xml(self) -> ET.Element: + testcase = ET.Element("testcase", self.attrs, time="%.3f" % self.duration) + properties = self.make_properties_node() + if properties is not None: + testcase.append(properties) + testcase.extend(self.nodes) return testcase - def _add_simple(self, kind: "Type[py.xml.Tag]", message: str, data=None) -> None: - data = bin_xml_escape(data) - node = kind(data, message=message) + def _add_simple(self, tag: str, message: str, data: Optional[str] = None) -> None: + node = ET.Element(tag, message=message) + node.text = bin_xml_escape(data) self.append(node) def write_captured_output(self, report: TestReport) -> None: @@ -203,8 +190,9 @@ def _prepare_content(self, content: str, header: str) -> str: return "\n".join([header.center(80, "-"), content, ""]) def _write_content(self, report: TestReport, content: str, jheader: str) -> None: - tag = getattr(Junit, jheader) - self.append(tag(bin_xml_escape(content))) + tag = ET.Element(jheader) + tag.text = bin_xml_escape(content) + self.append(tag) def append_pass(self, report: TestReport) -> None: self.add_stats("passed") @@ -212,7 +200,7 @@ def append_pass(self, report: TestReport) -> None: def append_failure(self, report: TestReport) -> None: # msg = str(report.longrepr.reprtraceback.extraline) if hasattr(report, "wasxfail"): - self._add_simple(Junit.skipped, "xfail-marked test passes unexpectedly") + self._add_simple("skipped", "xfail-marked test passes unexpectedly") else: assert report.longrepr is not None if getattr(report.longrepr, "reprcrash", None) is not None: @@ -220,19 +208,15 @@ def append_failure(self, report: TestReport) -> None: else: message = str(report.longrepr) message = bin_xml_escape(message) - fail = Junit.failure(message=message) - fail.append(bin_xml_escape(report.longrepr)) - self.append(fail) + self._add_simple("failure", message, str(report.longrepr)) def append_collect_error(self, report: TestReport) -> None: # msg = str(report.longrepr.reprtraceback.extraline) assert report.longrepr is not None - self.append( - Junit.error(bin_xml_escape(report.longrepr), message="collection failure") - ) + self._add_simple("error", "collection failure", str(report.longrepr)) def append_collect_skipped(self, report: TestReport) -> None: - self._add_simple(Junit.skipped, "collection skipped", report.longrepr) + self._add_simple("skipped", "collection skipped", str(report.longrepr)) def append_error(self, report: TestReport) -> None: assert report.longrepr is not None @@ -245,18 +229,16 @@ def append_error(self, report: TestReport) -> None: msg = 'failed on teardown with "{}"'.format(reason) else: msg = 'failed on setup with "{}"'.format(reason) - self._add_simple(Junit.error, msg, report.longrepr) + self._add_simple("error", msg, str(report.longrepr)) def append_skipped(self, report: TestReport) -> None: if hasattr(report, "wasxfail"): xfailreason = report.wasxfail if xfailreason.startswith("reason: "): xfailreason = xfailreason[8:] - self.append( - Junit.skipped( - "", type="pytest.xfail", message=bin_xml_escape(xfailreason) - ) - ) + xfailreason = bin_xml_escape(xfailreason) + skipped = ET.Element("skipped", type="pytest.xfail", message=xfailreason) + self.append(skipped) else: assert report.longrepr is not None filename, lineno, skipreason = report.longrepr @@ -264,21 +246,17 @@ def append_skipped(self, report: TestReport) -> None: skipreason = skipreason[9:] details = "{}:{}: {}".format(filename, lineno, skipreason) - self.append( - Junit.skipped( - bin_xml_escape(details), - type="pytest.skip", - message=bin_xml_escape(skipreason), - ) - ) + skipped = ET.Element("skipped", type="pytest.skip", message=skipreason) + skipped.text = bin_xml_escape(details) + self.append(skipped) self.write_captured_output(report) def finalize(self) -> None: - data = self.to_xml().unicode(indent=0) + data = self.to_xml() self.__dict__.clear() # Type ignored becuase mypy doesn't like overriding a method. # Also the return value doesn't match... - self.to_xml = lambda: py.xml.raw(data) # type: ignore + self.to_xml = lambda: data # type: ignore[assignment] def _warn_incompatibility_with_xunit2( @@ -502,7 +480,7 @@ def __init__( {} ) # type: Dict[Tuple[Union[str, TestReport], object], _NodeReporter] self.node_reporters_ordered = [] # type: List[_NodeReporter] - self.global_properties = [] # type: List[Tuple[str, py.xml.raw]] + self.global_properties = [] # type: List[Tuple[str, str]] # List of reports that failed on call but teardown is pending. self.open_reports = [] # type: List[TestReport] @@ -654,7 +632,7 @@ def pytest_collectreport(self, report: TestReport) -> None: def pytest_internalerror(self, excrepr: ExceptionRepr) -> None: reporter = self.node_reporter("internal") reporter.attrs.update(classname="pytest", name="internal") - reporter._add_simple(Junit.error, "internal error", excrepr) + reporter._add_simple("error", "internal error", str(excrepr)) def pytest_sessionstart(self) -> None: self.suite_start_time = timing.time() @@ -676,9 +654,8 @@ def pytest_sessionfinish(self) -> None: ) logfile.write('') - suite_node = Junit.testsuite( - self._get_global_properties_node(), - [x.to_xml() for x in self.node_reporters_ordered], + suite_node = ET.Element( + "testsuite", name=self.suite_name, errors=str(self.stats["error"]), failures=str(self.stats["failure"]), @@ -688,7 +665,14 @@ def pytest_sessionfinish(self) -> None: timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(), hostname=platform.node(), ) - logfile.write(Junit.testsuites([suite_node]).unicode(indent=0)) + global_properties = self._get_global_properties_node() + if global_properties is not None: + suite_node.append(global_properties) + for node_reporter in self.node_reporters_ordered: + suite_node.append(node_reporter.to_xml()) + testsuites = ET.Element("testsuites") + testsuites.append(suite_node) + logfile.write(ET.tostring(testsuites, encoding="unicode")) logfile.close() def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None: @@ -699,14 +683,12 @@ def add_global_property(self, name: str, value: object) -> None: _check_record_param_type("name", name) self.global_properties.append((name, bin_xml_escape(value))) - def _get_global_properties_node(self) -> Union[py.xml.Tag, str]: + def _get_global_properties_node(self) -> Optional[ET.Element]: """Return a Junit node containing custom properties, if any. """ if self.global_properties: - return Junit.properties( - [ - Junit.property(name=name, value=value) - for name, value in self.global_properties - ] - ) - return "" + properties = ET.Element("properties") + for name, value in self.global_properties: + properties.append(ET.Element("property", name=name, value=value)) + return properties + return None diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 01eeccdcd92..5487940fbcd 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -323,8 +323,9 @@ def test_function(arg): node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, failures=1, tests=1) first, second = dom.find_by_tag("testcase") - if not first or not second or first == second: - assert 0 + assert first + assert second + assert first != second fnode = first.find_first_by_tag("failure") fnode.assert_attr(message="Exception: Call Exception") snode = second.find_first_by_tag("error") @@ -535,7 +536,7 @@ def test_fail(): node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") fnode = tnode.find_first_by_tag("failure") - fnode.assert_attr(message="AssertionError: An error assert 0") + fnode.assert_attr(message="AssertionError: An error\nassert 0") @parametrize_families def test_failure_escape(self, testdir, run_and_parse, xunit_family): @@ -995,14 +996,14 @@ def test_invalid_xml_escape(): # 0xD, 0xD7FF, 0xE000, 0xFFFD, 0x10000, 0x10FFFF) for i in invalid: - got = bin_xml_escape(chr(i)).uniobj + got = bin_xml_escape(chr(i)) if i <= 0xFF: expected = "#x%02X" % i else: expected = "#x%04X" % i assert got == expected for i in valid: - assert chr(i) == bin_xml_escape(chr(i)).uniobj + assert chr(i) == bin_xml_escape(chr(i)) def test_logxml_path_expansion(tmpdir, monkeypatch): From c7558407934ff4a9d02bc73282858e07ef581cd8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 24 Jul 2020 10:20:37 +0300 Subject: [PATCH 513/823] pre-commit: extend list of rejected py modules We now only use `py.path.local`. --- .pre-commit-config.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc371720417..817cee60406 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,9 +70,11 @@ repos: _code\.| builtin\.| code\.| - io\.(BytesIO|saferepr|TerminalWriter)| + io\.| path\.local\.sysfind| process\.| - std\. + std\.| + error\.| + xml\. ) types: [python] From edb6211e3692974dc2b0984d71483f3296fcef3f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Jul 2020 11:49:41 +0300 Subject: [PATCH 514/823] Merge pull request #7565 from bluetech/pylint-callable-2 mark: fix pylint not-callable error on pytest.mark.parametrize(...), again (cherry picked from commit f9837f953c03268baa4ae8e9803cb3a13ec6860c) --- changelog/7558.bugfix.rst | 2 ++ src/_pytest/mark/structures.py | 14 ++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 changelog/7558.bugfix.rst diff --git a/changelog/7558.bugfix.rst b/changelog/7558.bugfix.rst new file mode 100644 index 00000000000..6e3ec674c9b --- /dev/null +++ b/changelog/7558.bugfix.rst @@ -0,0 +1,2 @@ +Fix pylint ``not-callable`` lint on ``pytest.mark.parametrize()`` and the other builtin marks: +``skip``, ``skipif``, ``xfail``, ``usefixtures``, ``filterwarnings``. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 5edeecdd5a4..9f8ce4ebce3 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -4,7 +4,6 @@ import warnings from typing import Any from typing import Callable -from typing import cast from typing import Iterable from typing import List from typing import Mapping @@ -473,14 +472,13 @@ def test_function(): # See TYPE_CHECKING above. if TYPE_CHECKING: - # Using casts instead of type comments intentionally - issue #7473. # TODO(py36): Change to builtin annotation syntax. - skip = cast(_SkipMarkDecorator, None) - skipif = cast(_SkipifMarkDecorator, None) - xfail = cast(_XfailMarkDecorator, None) - parametrize = cast(_ParametrizeMarkDecorator, None) - usefixtures = cast(_UsefixturesMarkDecorator, None) - filterwarnings = cast(_FilterwarningsMarkDecorator, None) + skip = _SkipMarkDecorator(Mark("skip", (), {})) + skipif = _SkipifMarkDecorator(Mark("skipif", (), {})) + xfail = _XfailMarkDecorator(Mark("xfail", (), {})) + parametrize = _ParametrizeMarkDecorator(Mark("parametrize ", (), {})) + usefixtures = _UsefixturesMarkDecorator(Mark("usefixtures ", (), {})) + filterwarnings = _FilterwarningsMarkDecorator(Mark("filterwarnings ", (), {})) def __getattr__(self, name: str) -> MarkDecorator: if name[0] == "_": From 54e08b72304908b0cddb1ec68c297815cbd7f757 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Jul 2020 11:58:54 +0300 Subject: [PATCH 515/823] mark: fix extraneous spaces in dummy type-checking marks --- src/_pytest/mark/structures.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 9f8ce4ebce3..6567822999a 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -476,9 +476,9 @@ def test_function(): skip = _SkipMarkDecorator(Mark("skip", (), {})) skipif = _SkipifMarkDecorator(Mark("skipif", (), {})) xfail = _XfailMarkDecorator(Mark("xfail", (), {})) - parametrize = _ParametrizeMarkDecorator(Mark("parametrize ", (), {})) - usefixtures = _UsefixturesMarkDecorator(Mark("usefixtures ", (), {})) - filterwarnings = _FilterwarningsMarkDecorator(Mark("filterwarnings ", (), {})) + parametrize = _ParametrizeMarkDecorator(Mark("parametrize", (), {})) + usefixtures = _UsefixturesMarkDecorator(Mark("usefixtures", (), {})) + filterwarnings = _FilterwarningsMarkDecorator(Mark("filterwarnings", (), {})) def __getattr__(self, name: str) -> MarkDecorator: if name[0] == "_": From bec1bdaa2cf53bc6213a106edec1131287adc22d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Jul 2020 11:58:54 +0300 Subject: [PATCH 516/823] mark: fix extraneous spaces in dummy type-checking marks (cherry picked from commit 54e08b72304908b0cddb1ec68c297815cbd7f757) --- src/_pytest/mark/structures.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 9f8ce4ebce3..6567822999a 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -476,9 +476,9 @@ def test_function(): skip = _SkipMarkDecorator(Mark("skip", (), {})) skipif = _SkipifMarkDecorator(Mark("skipif", (), {})) xfail = _XfailMarkDecorator(Mark("xfail", (), {})) - parametrize = _ParametrizeMarkDecorator(Mark("parametrize ", (), {})) - usefixtures = _UsefixturesMarkDecorator(Mark("usefixtures ", (), {})) - filterwarnings = _FilterwarningsMarkDecorator(Mark("filterwarnings ", (), {})) + parametrize = _ParametrizeMarkDecorator(Mark("parametrize", (), {})) + usefixtures = _UsefixturesMarkDecorator(Mark("usefixtures", (), {})) + filterwarnings = _FilterwarningsMarkDecorator(Mark("filterwarnings", (), {})) def __getattr__(self, name: str) -> MarkDecorator: if name[0] == "_": From 0e0275d8d999863921121d08baab8231f8fa27d8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 29 Jul 2020 14:46:11 +0300 Subject: [PATCH 517/823] logging: fix capture handler level not reset on teardown after caplog.set_level() This probably regressed in fcbaab8. --- src/_pytest/logging.py | 4 ++++ testing/logging/test_fixture.py | 35 +++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 11031f2f229..0ee9457ea72 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -345,6 +345,7 @@ def __init__(self, item: nodes.Node) -> None: """Creates a new funcarg.""" self._item = item # dict of log name -> log level + self._initial_handler_level = None # type: Optional[int] self._initial_logger_levels = {} # type: Dict[Optional[str], int] def _finalize(self) -> None: @@ -353,6 +354,8 @@ def _finalize(self) -> None: This restores the log levels changed by :meth:`set_level`. """ # restore log levels + if self._initial_handler_level is not None: + self.handler.setLevel(self._initial_handler_level) for logger_name, level in self._initial_logger_levels.items(): logger = logging.getLogger(logger_name) logger.setLevel(level) @@ -434,6 +437,7 @@ def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> Non # save the original log-level to restore it during teardown self._initial_logger_levels.setdefault(logger, logger_obj.level) logger_obj.setLevel(level) + self._initial_handler_level = self.handler.level self.handler.setLevel(level) @contextmanager diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index da5303302b6..6e5e9c2b42a 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -2,6 +2,7 @@ import pytest from _pytest.logging import caplog_records_key +from _pytest.pytester import Testdir logger = logging.getLogger(__name__) sublogger = logging.getLogger(__name__ + ".baz") @@ -27,8 +28,11 @@ def test_change_level(caplog): assert "CRITICAL" in caplog.text -def test_change_level_undo(testdir): - """Ensure that 'set_level' is undone after the end of the test""" +def test_change_level_undo(testdir: Testdir) -> None: + """Ensure that 'set_level' is undone after the end of the test. + + Tests the logging output themselves (affacted both by logger and handler levels). + """ testdir.makepyfile( """ import logging @@ -50,6 +54,33 @@ def test2(caplog): result.stdout.no_fnmatch_line("*log from test2*") +def test_change_level_undos_handler_level(testdir: Testdir) -> None: + """Ensure that 'set_level' is undone after the end of the test (handler). + + Issue #7569. Tests the handler level specifically. + """ + testdir.makepyfile( + """ + import logging + + def test1(caplog): + assert caplog.handler.level == 0 + caplog.set_level(41) + assert caplog.handler.level == 41 + + def test2(caplog): + assert caplog.handler.level == 0 + + def test3(caplog): + assert caplog.handler.level == 0 + caplog.set_level(43) + assert caplog.handler.level == 43 + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=3) + + def test_with_statement(caplog): with caplog.at_level(logging.INFO): logger.debug("handler DEBUG level") From d3267bc49d425f047cc5584d9030b145454890dc Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 28 Jul 2020 21:02:23 -0300 Subject: [PATCH 518/823] Fix TestReport.longreprtext when TestReport.longrepr is not a string Fix #7559 --- changelog/7559.bugfix.rst | 1 + src/_pytest/reports.py | 5 +++-- testing/test_runner.py | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 changelog/7559.bugfix.rst diff --git a/changelog/7559.bugfix.rst b/changelog/7559.bugfix.rst new file mode 100644 index 00000000000..b3d98826b7e --- /dev/null +++ b/changelog/7559.bugfix.rst @@ -0,0 +1 @@ +Fix regression in plugins using ``TestReport.longreprtext`` (such as ``pytest-html``) when ``TestReport.longrepr`` is not a string. diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 186c53ed321..cbd9ae1832a 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -82,9 +82,10 @@ def toterminal(self, out: TerminalWriter) -> None: longrepr.toterminal(out) else: try: - out.line(longrepr) + s = str(longrepr) except UnicodeEncodeError: - out.line("") + s = "" + out.line(s) def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]: for name, content in self.sections: diff --git a/testing/test_runner.py b/testing/test_runner.py index def3f910d52..b207ccc927f 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -951,6 +951,33 @@ def test_func(): rep = reports[1] assert rep.longreprtext == "" + def test_longreprtext_skip(self, testdir) -> None: + """TestReport.longreprtext can handle non-str ``longrepr`` attributes (#7559)""" + reports = testdir.runitem( + """ + import pytest + def test_func(): + pytest.skip() + """ + ) + _, call_rep, _ = reports + assert isinstance(call_rep.longrepr, tuple) + assert "Skipped" in call_rep.longreprtext + + def test_longreprtext_collect_skip(self, testdir) -> None: + """CollectReport.longreprtext can handle non-str ``longrepr`` attributes (#7559)""" + testdir.makepyfile( + """ + import pytest + pytest.skip(allow_module_level=True) + """ + ) + rec = testdir.inline_run() + calls = rec.getcalls("pytest_collectreport") + _, call = calls + assert isinstance(call.report.longrepr, tuple) + assert "Skipped" in call.report.longreprtext + def test_longreprtext_failure(self, testdir) -> None: reports = testdir.runitem( """ From f9d5f6e60a3e64fbdc5b03ceff3eeb56ddeb9c7d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 29 Jul 2020 09:37:57 -0300 Subject: [PATCH 519/823] Merge pull request #7571 from bluetech/logging-setlevel-handler-restore logging: fix capture handler level not reset on teardown after caplog.set_level() --- src/_pytest/logging.py | 4 ++++ testing/logging/test_fixture.py | 35 +++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 11031f2f229..0ee9457ea72 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -345,6 +345,7 @@ def __init__(self, item: nodes.Node) -> None: """Creates a new funcarg.""" self._item = item # dict of log name -> log level + self._initial_handler_level = None # type: Optional[int] self._initial_logger_levels = {} # type: Dict[Optional[str], int] def _finalize(self) -> None: @@ -353,6 +354,8 @@ def _finalize(self) -> None: This restores the log levels changed by :meth:`set_level`. """ # restore log levels + if self._initial_handler_level is not None: + self.handler.setLevel(self._initial_handler_level) for logger_name, level in self._initial_logger_levels.items(): logger = logging.getLogger(logger_name) logger.setLevel(level) @@ -434,6 +437,7 @@ def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> Non # save the original log-level to restore it during teardown self._initial_logger_levels.setdefault(logger, logger_obj.level) logger_obj.setLevel(level) + self._initial_handler_level = self.handler.level self.handler.setLevel(level) @contextmanager diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index da5303302b6..6e5e9c2b42a 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -2,6 +2,7 @@ import pytest from _pytest.logging import caplog_records_key +from _pytest.pytester import Testdir logger = logging.getLogger(__name__) sublogger = logging.getLogger(__name__ + ".baz") @@ -27,8 +28,11 @@ def test_change_level(caplog): assert "CRITICAL" in caplog.text -def test_change_level_undo(testdir): - """Ensure that 'set_level' is undone after the end of the test""" +def test_change_level_undo(testdir: Testdir) -> None: + """Ensure that 'set_level' is undone after the end of the test. + + Tests the logging output themselves (affacted both by logger and handler levels). + """ testdir.makepyfile( """ import logging @@ -50,6 +54,33 @@ def test2(caplog): result.stdout.no_fnmatch_line("*log from test2*") +def test_change_level_undos_handler_level(testdir: Testdir) -> None: + """Ensure that 'set_level' is undone after the end of the test (handler). + + Issue #7569. Tests the handler level specifically. + """ + testdir.makepyfile( + """ + import logging + + def test1(caplog): + assert caplog.handler.level == 0 + caplog.set_level(41) + assert caplog.handler.level == 41 + + def test2(caplog): + assert caplog.handler.level == 0 + + def test3(caplog): + assert caplog.handler.level == 0 + caplog.set_level(43) + assert caplog.handler.level == 43 + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=3) + + def test_with_statement(caplog): with caplog.at_level(logging.INFO): logger.debug("handler DEBUG level") From fe252848c56e081fc7eb1b98cd60e33ee33b7dd7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 29 Jul 2020 09:47:04 -0300 Subject: [PATCH 520/823] Merge pull request #7561 from nicoddemus/longreprtext-7559 --- changelog/7559.bugfix.rst | 1 + src/_pytest/reports.py | 5 +++-- testing/test_runner.py | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 changelog/7559.bugfix.rst diff --git a/changelog/7559.bugfix.rst b/changelog/7559.bugfix.rst new file mode 100644 index 00000000000..b3d98826b7e --- /dev/null +++ b/changelog/7559.bugfix.rst @@ -0,0 +1 @@ +Fix regression in plugins using ``TestReport.longreprtext`` (such as ``pytest-html``) when ``TestReport.longrepr`` is not a string. diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 186c53ed321..cbd9ae1832a 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -82,9 +82,10 @@ def toterminal(self, out: TerminalWriter) -> None: longrepr.toterminal(out) else: try: - out.line(longrepr) + s = str(longrepr) except UnicodeEncodeError: - out.line("") + s = "" + out.line(s) def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]: for name, content in self.sections: diff --git a/testing/test_runner.py b/testing/test_runner.py index def3f910d52..b207ccc927f 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -951,6 +951,33 @@ def test_func(): rep = reports[1] assert rep.longreprtext == "" + def test_longreprtext_skip(self, testdir) -> None: + """TestReport.longreprtext can handle non-str ``longrepr`` attributes (#7559)""" + reports = testdir.runitem( + """ + import pytest + def test_func(): + pytest.skip() + """ + ) + _, call_rep, _ = reports + assert isinstance(call_rep.longrepr, tuple) + assert "Skipped" in call_rep.longreprtext + + def test_longreprtext_collect_skip(self, testdir) -> None: + """CollectReport.longreprtext can handle non-str ``longrepr`` attributes (#7559)""" + testdir.makepyfile( + """ + import pytest + pytest.skip(allow_module_level=True) + """ + ) + rec = testdir.inline_run() + calls = rec.getcalls("pytest_collectreport") + _, call = calls + assert isinstance(call.report.longrepr, tuple) + assert "Skipped" in call.report.longreprtext + def test_longreprtext_failure(self, testdir) -> None: reports = testdir.runitem( """ From 22acbaf3934979cc916a932263a7dd41810a1b1c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 29 Jul 2020 11:29:24 -0300 Subject: [PATCH 521/823] Minor changes to the release process As discussed in https://github.com/pytest-dev/pytest/pull/7556 --- RELEASING.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASING.rst b/RELEASING.rst index f5e2528e3f2..5893987e37f 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -69,9 +69,9 @@ Both automatic and manual processes described above follow the same steps from t git fetch --all --prune git checkout origin/master -b cherry-pick-release - git cherry-pick --no-commit -m1 origin/MAJOR.MINOR.x - git checkout origin/master -- changelog - git commit # no arguments + git cherry-pick -x -m1 origin/MAJOR.MINOR.x + +#. Open a PR for ``cherry-pick-release`` and merge it once CI passes. No need to wait for approvals if there were no conflicts on the previous step. #. Send an email announcement with the contents from:: From 1e66ed0b1caee2e58cbafce80fc177330a19126e Mon Sep 17 00:00:00 2001 From: Mattreex <45504048+mattreex@users.noreply.github.com> Date: Wed, 29 Jul 2020 09:58:18 -0500 Subject: [PATCH 522/823] Warn about --basetemp removing the entire directory (#7555) Co-authored-by: mattreex Co-authored-by: Bruno Oliveira --- doc/en/tmpdir.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index a3749d855a4..5f882b1400f 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -192,8 +192,13 @@ You can override the default temporary directory setting like this: pytest --basetemp=mydir -When distributing tests on the local machine, ``pytest`` takes care to -configure a basetemp directory for the sub processes such that all temporary +.. warning:: + + The contents of ``mydir`` will be completely removed, so make sure to use a directory + for that purpose only. + +When distributing tests on the local machine using ``pytest-xdist``, care is taken to +automatically configure a basetemp directory for the sub processes such that all temporary data lands below a single per-test run basetemp directory. .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html From d756b4a5432bddcae26869ff902ca9457fe59f8c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 29 Jul 2020 18:19:33 +0300 Subject: [PATCH 523/823] Fix typo: remove stray indefinite article from release notes (#7552) Co-authored-by: Bruno Oliveira --- doc/en/announce/release-2.5.1.rst | 2 +- doc/en/announce/release-2.5.2.rst | 2 +- doc/en/announce/release-2.6.0.rst | 2 +- doc/en/announce/release-2.6.1.rst | 2 +- doc/en/announce/release-2.6.2.rst | 2 +- doc/en/announce/release-2.6.3.rst | 2 +- doc/en/announce/release-2.7.0.rst | 2 +- doc/en/announce/release-2.7.1.rst | 2 +- doc/en/announce/release-2.7.2.rst | 2 +- doc/en/announce/release-2.8.2.rst | 2 +- doc/en/announce/release-2.8.3.rst | 2 +- doc/en/announce/release-2.8.4.rst | 2 +- doc/en/announce/release-2.8.5.rst | 2 +- doc/en/announce/release-2.8.6.rst | 2 +- doc/en/announce/release-2.8.7.rst | 2 +- doc/en/announce/release-2.9.0.rst | 2 +- doc/en/announce/release-2.9.1.rst | 2 +- doc/en/announce/release-2.9.2.rst | 2 +- doc/en/announce/release-3.0.0.rst | 2 +- doc/en/announce/release-3.1.0.rst | 2 +- doc/en/announce/release-3.10.0.rst | 2 +- doc/en/announce/release-3.2.0.rst | 2 +- doc/en/announce/release-3.3.0.rst | 2 +- doc/en/announce/release-3.4.0.rst | 2 +- doc/en/announce/release-3.5.0.rst | 2 +- doc/en/announce/release-3.6.0.rst | 2 +- doc/en/announce/release-3.7.0.rst | 2 +- doc/en/announce/release-3.8.0.rst | 2 +- doc/en/announce/release-3.9.0.rst | 2 +- doc/en/announce/release-4.0.0.rst | 2 +- doc/en/announce/release-4.1.0.rst | 2 +- doc/en/announce/release-4.2.0.rst | 2 +- doc/en/announce/release-4.3.0.rst | 2 +- doc/en/announce/release-4.4.0.rst | 2 +- doc/en/announce/release-4.5.0.rst | 2 +- doc/en/announce/release-4.6.0.rst | 2 +- doc/en/announce/release-5.0.0.rst | 2 +- doc/en/announce/release-5.1.0.rst | 2 +- doc/en/announce/release-5.2.0.rst | 2 +- doc/en/announce/release-5.3.0.rst | 2 +- doc/en/announce/release-5.4.0.rst | 2 +- 41 files changed, 41 insertions(+), 41 deletions(-) diff --git a/doc/en/announce/release-2.5.1.rst b/doc/en/announce/release-2.5.1.rst index 22e69a836b9..ff39db2d52d 100644 --- a/doc/en/announce/release-2.5.1.rst +++ b/doc/en/announce/release-2.5.1.rst @@ -1,7 +1,7 @@ pytest-2.5.1: fixes and new home page styling =========================================================================== -pytest is a mature Python testing tool with more than a 1000 tests +pytest is a mature Python testing tool with more than 1000 tests against itself, passing on many different interpreters and platforms. The 2.5.1 release maintains the "zero-reported-bugs" promise by fixing diff --git a/doc/en/announce/release-2.5.2.rst b/doc/en/announce/release-2.5.2.rst index c389f5f5403..edc4da6e19f 100644 --- a/doc/en/announce/release-2.5.2.rst +++ b/doc/en/announce/release-2.5.2.rst @@ -1,7 +1,7 @@ pytest-2.5.2: fixes =========================================================================== -pytest is a mature Python testing tool with more than a 1000 tests +pytest is a mature Python testing tool with more than 1000 tests against itself, passing on many different interpreters and platforms. The 2.5.2 release fixes a few bugs with two maybe-bugs remaining and diff --git a/doc/en/announce/release-2.6.0.rst b/doc/en/announce/release-2.6.0.rst index 36b545a28b4..56fbd6cc1e4 100644 --- a/doc/en/announce/release-2.6.0.rst +++ b/doc/en/announce/release-2.6.0.rst @@ -1,7 +1,7 @@ pytest-2.6.0: shorter tracebacks, new warning system, test runner compat =========================================================================== -pytest is a mature Python testing tool with more than a 1000 tests +pytest is a mature Python testing tool with more than 1000 tests against itself, passing on many different interpreters and platforms. The 2.6.0 release should be drop-in backward compatible to 2.5.2 and diff --git a/doc/en/announce/release-2.6.1.rst b/doc/en/announce/release-2.6.1.rst index 85d9861643a..7469c488e5f 100644 --- a/doc/en/announce/release-2.6.1.rst +++ b/doc/en/announce/release-2.6.1.rst @@ -1,7 +1,7 @@ pytest-2.6.1: fixes and new xfail feature =========================================================================== -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. The 2.6.1 release is drop-in compatible to 2.5.2 and actually fixes some regressions introduced with 2.6.0. It also brings a little feature diff --git a/doc/en/announce/release-2.6.2.rst b/doc/en/announce/release-2.6.2.rst index f6ce178a107..9c3b7d96b07 100644 --- a/doc/en/announce/release-2.6.2.rst +++ b/doc/en/announce/release-2.6.2.rst @@ -1,7 +1,7 @@ pytest-2.6.2: few fixes and cx_freeze support =========================================================================== -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. This release is drop-in compatible to 2.5.2 and 2.6.X. It also brings support for including pytest with cx_freeze or similar diff --git a/doc/en/announce/release-2.6.3.rst b/doc/en/announce/release-2.6.3.rst index 7353dfee71c..56973a2b2f7 100644 --- a/doc/en/announce/release-2.6.3.rst +++ b/doc/en/announce/release-2.6.3.rst @@ -1,7 +1,7 @@ pytest-2.6.3: fixes and little improvements =========================================================================== -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. This release is drop-in compatible to 2.5.2 and 2.6.X. See below for the changes and see docs at: diff --git a/doc/en/announce/release-2.7.0.rst b/doc/en/announce/release-2.7.0.rst index 2f6d50d8b69..2840178a07f 100644 --- a/doc/en/announce/release-2.7.0.rst +++ b/doc/en/announce/release-2.7.0.rst @@ -1,7 +1,7 @@ pytest-2.7.0: fixes, features, speed improvements =========================================================================== -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. This release is supposed to be drop-in compatible to 2.6.X. diff --git a/doc/en/announce/release-2.7.1.rst b/doc/en/announce/release-2.7.1.rst index fdc71eebba9..5110c085e01 100644 --- a/doc/en/announce/release-2.7.1.rst +++ b/doc/en/announce/release-2.7.1.rst @@ -1,7 +1,7 @@ pytest-2.7.1: bug fixes ======================= -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. This release is supposed to be drop-in compatible to 2.7.0. diff --git a/doc/en/announce/release-2.7.2.rst b/doc/en/announce/release-2.7.2.rst index 1e3950de4d0..93e5b64eeed 100644 --- a/doc/en/announce/release-2.7.2.rst +++ b/doc/en/announce/release-2.7.2.rst @@ -1,7 +1,7 @@ pytest-2.7.2: bug fixes ======================= -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. This release is supposed to be drop-in compatible to 2.7.1. diff --git a/doc/en/announce/release-2.8.2.rst b/doc/en/announce/release-2.8.2.rst index d7028616142..e4726338852 100644 --- a/doc/en/announce/release-2.8.2.rst +++ b/doc/en/announce/release-2.8.2.rst @@ -1,7 +1,7 @@ pytest-2.8.2: bug fixes ======================= -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. This release is supposed to be drop-in compatible to 2.8.1. diff --git a/doc/en/announce/release-2.8.3.rst b/doc/en/announce/release-2.8.3.rst index b131a7e1f14..3f357252bb6 100644 --- a/doc/en/announce/release-2.8.3.rst +++ b/doc/en/announce/release-2.8.3.rst @@ -1,7 +1,7 @@ pytest-2.8.3: bug fixes ======================= -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. This release is supposed to be drop-in compatible to 2.8.2. diff --git a/doc/en/announce/release-2.8.4.rst b/doc/en/announce/release-2.8.4.rst index a09629cef09..adbdecc87ea 100644 --- a/doc/en/announce/release-2.8.4.rst +++ b/doc/en/announce/release-2.8.4.rst @@ -1,7 +1,7 @@ pytest-2.8.4 ============ -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. This release is supposed to be drop-in compatible to 2.8.2. diff --git a/doc/en/announce/release-2.8.5.rst b/doc/en/announce/release-2.8.5.rst index 7409022a137..c5343d1ea72 100644 --- a/doc/en/announce/release-2.8.5.rst +++ b/doc/en/announce/release-2.8.5.rst @@ -1,7 +1,7 @@ pytest-2.8.5 ============ -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. This release is supposed to be drop-in compatible to 2.8.4. diff --git a/doc/en/announce/release-2.8.6.rst b/doc/en/announce/release-2.8.6.rst index 215fae51eac..5d6565b16a3 100644 --- a/doc/en/announce/release-2.8.6.rst +++ b/doc/en/announce/release-2.8.6.rst @@ -1,7 +1,7 @@ pytest-2.8.6 ============ -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. This release is supposed to be drop-in compatible to 2.8.5. diff --git a/doc/en/announce/release-2.8.7.rst b/doc/en/announce/release-2.8.7.rst index 9005f56363a..8236a096669 100644 --- a/doc/en/announce/release-2.8.7.rst +++ b/doc/en/announce/release-2.8.7.rst @@ -4,7 +4,7 @@ pytest-2.8.7 This is a hotfix release to solve a regression in the builtin monkeypatch plugin that got introduced in 2.8.6. -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. This release is supposed to be drop-in compatible to 2.8.5. diff --git a/doc/en/announce/release-2.9.0.rst b/doc/en/announce/release-2.9.0.rst index f5d4be71347..8c2ee05f9bf 100644 --- a/doc/en/announce/release-2.9.0.rst +++ b/doc/en/announce/release-2.9.0.rst @@ -1,7 +1,7 @@ pytest-2.9.0 ============ -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. See below for the changes and see docs at: diff --git a/doc/en/announce/release-2.9.1.rst b/doc/en/announce/release-2.9.1.rst index c71f3851638..47bc2e6d38b 100644 --- a/doc/en/announce/release-2.9.1.rst +++ b/doc/en/announce/release-2.9.1.rst @@ -1,7 +1,7 @@ pytest-2.9.1 ============ -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. See below for the changes and see docs at: diff --git a/doc/en/announce/release-2.9.2.rst b/doc/en/announce/release-2.9.2.rst index b007a6d99e8..ffd8dc58ed5 100644 --- a/doc/en/announce/release-2.9.2.rst +++ b/doc/en/announce/release-2.9.2.rst @@ -1,7 +1,7 @@ pytest-2.9.2 ============ -pytest is a mature Python testing tool with more than a 1100 tests +pytest is a mature Python testing tool with more than 1100 tests against itself, passing on many different interpreters and platforms. See below for the changes and see docs at: diff --git a/doc/en/announce/release-3.0.0.rst b/doc/en/announce/release-3.0.0.rst index ca3e9e32763..5de38911482 100644 --- a/doc/en/announce/release-3.0.0.rst +++ b/doc/en/announce/release-3.0.0.rst @@ -3,7 +3,7 @@ pytest-3.0.0 The pytest team is proud to announce the 3.0.0 release! -pytest is a mature Python testing tool with more than a 1600 tests +pytest is a mature Python testing tool with more than 1600 tests against itself, passing on many different interpreters and platforms. This release contains a lot of bugs fixes and improvements, and much of diff --git a/doc/en/announce/release-3.1.0.rst b/doc/en/announce/release-3.1.0.rst index b84fd4c3cf9..55277067948 100644 --- a/doc/en/announce/release-3.1.0.rst +++ b/doc/en/announce/release-3.1.0.rst @@ -3,7 +3,7 @@ pytest-3.1.0 The pytest team is proud to announce the 3.1.0 release! -pytest is a mature Python testing tool with more than a 1600 tests +pytest is a mature Python testing tool with more than 1600 tests against itself, passing on many different interpreters and platforms. This release contains a bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-3.10.0.rst b/doc/en/announce/release-3.10.0.rst index c16c381e8d0..ff3c000b0e7 100644 --- a/doc/en/announce/release-3.10.0.rst +++ b/doc/en/announce/release-3.10.0.rst @@ -3,7 +3,7 @@ pytest-3.10.0 The pytest team is proud to announce the 3.10.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-3.2.0.rst b/doc/en/announce/release-3.2.0.rst index 4d5d6f1671f..edc66a28e78 100644 --- a/doc/en/announce/release-3.2.0.rst +++ b/doc/en/announce/release-3.2.0.rst @@ -3,7 +3,7 @@ pytest-3.2.0 The pytest team is proud to announce the 3.2.0 release! -pytest is a mature Python testing tool with more than a 1600 tests +pytest is a mature Python testing tool with more than 1600 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-3.3.0.rst b/doc/en/announce/release-3.3.0.rst index e57bbac6a91..1cbf2c448c8 100644 --- a/doc/en/announce/release-3.3.0.rst +++ b/doc/en/announce/release-3.3.0.rst @@ -3,7 +3,7 @@ pytest-3.3.0 The pytest team is proud to announce the 3.3.0 release! -pytest is a mature Python testing tool with more than a 1600 tests +pytest is a mature Python testing tool with more than 1600 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-3.4.0.rst b/doc/en/announce/release-3.4.0.rst index ec6725370cd..6ab5b124a25 100644 --- a/doc/en/announce/release-3.4.0.rst +++ b/doc/en/announce/release-3.4.0.rst @@ -3,7 +3,7 @@ pytest-3.4.0 The pytest team is proud to announce the 3.4.0 release! -pytest is a mature Python testing tool with more than a 1600 tests +pytest is a mature Python testing tool with more than 1600 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-3.5.0.rst b/doc/en/announce/release-3.5.0.rst index ef64dc381e6..6bc2f3cd0cb 100644 --- a/doc/en/announce/release-3.5.0.rst +++ b/doc/en/announce/release-3.5.0.rst @@ -3,7 +3,7 @@ pytest-3.5.0 The pytest team is proud to announce the 3.5.0 release! -pytest is a mature Python testing tool with more than a 1600 tests +pytest is a mature Python testing tool with more than 1600 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-3.6.0.rst b/doc/en/announce/release-3.6.0.rst index 38a8b9e3f09..44b178c169f 100644 --- a/doc/en/announce/release-3.6.0.rst +++ b/doc/en/announce/release-3.6.0.rst @@ -3,7 +3,7 @@ pytest-3.6.0 The pytest team is proud to announce the 3.6.0 release! -pytest is a mature Python testing tool with more than a 1600 tests +pytest is a mature Python testing tool with more than 1600 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-3.7.0.rst b/doc/en/announce/release-3.7.0.rst index ef6c44f0aab..89908a9101c 100644 --- a/doc/en/announce/release-3.7.0.rst +++ b/doc/en/announce/release-3.7.0.rst @@ -3,7 +3,7 @@ pytest-3.7.0 The pytest team is proud to announce the 3.7.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-3.8.0.rst b/doc/en/announce/release-3.8.0.rst index 5369ffc7dba..8c35a44f6d5 100644 --- a/doc/en/announce/release-3.8.0.rst +++ b/doc/en/announce/release-3.8.0.rst @@ -3,7 +3,7 @@ pytest-3.8.0 The pytest team is proud to announce the 3.8.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-3.9.0.rst b/doc/en/announce/release-3.9.0.rst index 1d889e5bc85..0be6cf5be8a 100644 --- a/doc/en/announce/release-3.9.0.rst +++ b/doc/en/announce/release-3.9.0.rst @@ -3,7 +3,7 @@ pytest-3.9.0 The pytest team is proud to announce the 3.9.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-4.0.0.rst b/doc/en/announce/release-4.0.0.rst index b91fd59542d..5eb0107758a 100644 --- a/doc/en/announce/release-4.0.0.rst +++ b/doc/en/announce/release-4.0.0.rst @@ -3,7 +3,7 @@ pytest-4.0.0 The pytest team is proud to announce the 4.0.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-4.1.0.rst b/doc/en/announce/release-4.1.0.rst index 77aaf74af65..314564eeb6f 100644 --- a/doc/en/announce/release-4.1.0.rst +++ b/doc/en/announce/release-4.1.0.rst @@ -3,7 +3,7 @@ pytest-4.1.0 The pytest team is proud to announce the 4.1.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-4.2.0.rst b/doc/en/announce/release-4.2.0.rst index 11520acf331..bcd7f775479 100644 --- a/doc/en/announce/release-4.2.0.rst +++ b/doc/en/announce/release-4.2.0.rst @@ -3,7 +3,7 @@ pytest-4.2.0 The pytest team is proud to announce the 4.2.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-4.3.0.rst b/doc/en/announce/release-4.3.0.rst index 9dcbe415eec..3b0b4280922 100644 --- a/doc/en/announce/release-4.3.0.rst +++ b/doc/en/announce/release-4.3.0.rst @@ -3,7 +3,7 @@ pytest-4.3.0 The pytest team is proud to announce the 4.3.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-4.4.0.rst b/doc/en/announce/release-4.4.0.rst index d4abfac22a7..dc89739d0aa 100644 --- a/doc/en/announce/release-4.4.0.rst +++ b/doc/en/announce/release-4.4.0.rst @@ -3,7 +3,7 @@ pytest-4.4.0 The pytest team is proud to announce the 4.4.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-4.5.0.rst b/doc/en/announce/release-4.5.0.rst index 957e2c17285..d2a05d4f795 100644 --- a/doc/en/announce/release-4.5.0.rst +++ b/doc/en/announce/release-4.5.0.rst @@ -3,7 +3,7 @@ pytest-4.5.0 The pytest team is proud to announce the 4.5.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-4.6.0.rst b/doc/en/announce/release-4.6.0.rst index 7c7cf29d26a..a82fdd47d6f 100644 --- a/doc/en/announce/release-4.6.0.rst +++ b/doc/en/announce/release-4.6.0.rst @@ -3,7 +3,7 @@ pytest-4.6.0 The pytest team is proud to announce the 4.6.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-5.0.0.rst b/doc/en/announce/release-5.0.0.rst index 99c92a505fe..f5e593e9d88 100644 --- a/doc/en/announce/release-5.0.0.rst +++ b/doc/en/announce/release-5.0.0.rst @@ -3,7 +3,7 @@ pytest-5.0.0 The pytest team is proud to announce the 5.0.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-5.1.0.rst b/doc/en/announce/release-5.1.0.rst index 4293c581312..9ab54ff9730 100644 --- a/doc/en/announce/release-5.1.0.rst +++ b/doc/en/announce/release-5.1.0.rst @@ -3,7 +3,7 @@ pytest-5.1.0 The pytest team is proud to announce the 5.1.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-5.2.0.rst b/doc/en/announce/release-5.2.0.rst index fbe27031b1e..f43767b7506 100644 --- a/doc/en/announce/release-5.2.0.rst +++ b/doc/en/announce/release-5.2.0.rst @@ -3,7 +3,7 @@ pytest-5.2.0 The pytest team is proud to announce the 5.2.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-5.3.0.rst b/doc/en/announce/release-5.3.0.rst index 54c33e192b0..e13a71f09aa 100644 --- a/doc/en/announce/release-5.3.0.rst +++ b/doc/en/announce/release-5.3.0.rst @@ -3,7 +3,7 @@ pytest-5.3.0 The pytest team is proud to announce the 5.3.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bugs fixes and improvements, so users are encouraged diff --git a/doc/en/announce/release-5.4.0.rst b/doc/en/announce/release-5.4.0.rst index cb91e26ba36..43dffc9290e 100644 --- a/doc/en/announce/release-5.4.0.rst +++ b/doc/en/announce/release-5.4.0.rst @@ -3,7 +3,7 @@ pytest-5.4.0 The pytest team is proud to announce the 5.4.0 release! -pytest is a mature Python testing tool with more than a 2000 tests +pytest is a mature Python testing tool with more than 2000 tests against itself, passing on many different interpreters and platforms. This release contains a number of bug fixes and improvements, so users are encouraged From 924e466c983accf9fbf8e57ab744a862750b9d5b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Jul 2020 12:38:40 +0300 Subject: [PATCH 524/823] Add missing changelog for issue 7569 --- changelog/7569.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/7569.bugfix.rst diff --git a/changelog/7569.bugfix.rst b/changelog/7569.bugfix.rst new file mode 100644 index 00000000000..5dd38bc4d92 --- /dev/null +++ b/changelog/7569.bugfix.rst @@ -0,0 +1 @@ +Fix logging capture handler's level not reset on teardown after a call to ``caplog.set_level()``. From e49f1d6f60fc44e8d2ed76552c10d3bba3bf7a0f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 30 Jul 2020 09:45:40 -0300 Subject: [PATCH 525/823] Merge pull request #7584 from pytest-dev/release-6.0.1 Prepare release 6.0.1 (cherry picked from commit 022bff27a71406bd5dc4794d34f1fbbf56a45250) --- changelog/7394.bugfix.rst | 2 -- changelog/7558.bugfix.rst | 2 -- changelog/7559.bugfix.rst | 1 - changelog/7569.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-6.0.1.rst | 21 +++++++++++++++++++++ doc/en/changelog.rst | 20 ++++++++++++++++++++ doc/en/getting-started.rst | 2 +- 8 files changed, 43 insertions(+), 7 deletions(-) delete mode 100644 changelog/7394.bugfix.rst delete mode 100644 changelog/7558.bugfix.rst delete mode 100644 changelog/7559.bugfix.rst delete mode 100644 changelog/7569.bugfix.rst create mode 100644 doc/en/announce/release-6.0.1.rst diff --git a/changelog/7394.bugfix.rst b/changelog/7394.bugfix.rst deleted file mode 100644 index b39558cf1d2..00000000000 --- a/changelog/7394.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Passing an empty ``help`` value to ``Parser.add_option`` is now accepted instead of crashing when running ``pytest --help``. -Passing ``None`` raises a more informative ``TypeError``. diff --git a/changelog/7558.bugfix.rst b/changelog/7558.bugfix.rst deleted file mode 100644 index 6e3ec674c9b..00000000000 --- a/changelog/7558.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix pylint ``not-callable`` lint on ``pytest.mark.parametrize()`` and the other builtin marks: -``skip``, ``skipif``, ``xfail``, ``usefixtures``, ``filterwarnings``. diff --git a/changelog/7559.bugfix.rst b/changelog/7559.bugfix.rst deleted file mode 100644 index b3d98826b7e..00000000000 --- a/changelog/7559.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix regression in plugins using ``TestReport.longreprtext`` (such as ``pytest-html``) when ``TestReport.longrepr`` is not a string. diff --git a/changelog/7569.bugfix.rst b/changelog/7569.bugfix.rst deleted file mode 100644 index 5dd38bc4d92..00000000000 --- a/changelog/7569.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix logging capture handler's level not reset on teardown after a call to ``caplog.set_level()``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 7d176aa062c..49d7a0465c8 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.0.1 release-6.0.0 release-6.0.0rc1 release-5.4.3 diff --git a/doc/en/announce/release-6.0.1.rst b/doc/en/announce/release-6.0.1.rst new file mode 100644 index 00000000000..33fdbed3f61 --- /dev/null +++ b/doc/en/announce/release-6.0.1.rst @@ -0,0 +1,21 @@ +pytest-6.0.1 +======================================= + +pytest 6.0.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Bruno Oliveira +* Mattreex +* Ran Benita +* hp310780 + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 2ad8de2124a..7516b53f4ca 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,26 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 6.0.1 (2020-07-30) +========================= + +Bug Fixes +--------- + +- `#7394 `_: Passing an empty ``help`` value to ``Parser.add_option`` is now accepted instead of crashing when running ``pytest --help``. + Passing ``None`` raises a more informative ``TypeError``. + + +- `#7558 `_: Fix pylint ``not-callable`` lint on ``pytest.mark.parametrize()`` and the other builtin marks: + ``skip``, ``skipif``, ``xfail``, ``usefixtures``, ``filterwarnings``. + + +- `#7559 `_: Fix regression in plugins using ``TestReport.longreprtext`` (such as ``pytest-html``) when ``TestReport.longrepr`` is not a string. + + +- `#7569 `_: Fix logging capture handler's level not reset on teardown after a call to ``caplog.set_level()``. + + pytest 6.0.0 (2020-07-28) ========================= diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index a2f6daa392a..c160191b654 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 6.0.0 + pytest 6.0.1 .. _`simpletest`: From 96a48f0c66ebe1ec2305c21390a3f6c059760af5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 30 Jul 2020 17:05:32 +0300 Subject: [PATCH 526/823] Stop using more-itertools We barely use it; the couple places that do are not really worth the extra dependency, I think the code is clearer without it. Also simplifies one (regular) itertools usage. Also improves a check and an error message in `pytest.raises`. --- changelog/7587.trivial.rst | 1 + setup.cfg | 1 - src/_pytest/fixtures.py | 7 +++---- src/_pytest/python_api.py | 22 ++++++++++------------ src/_pytest/terminal.py | 12 +++++++----- testing/python/raises.py | 19 +++++++++++++++++++ 6 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 changelog/7587.trivial.rst diff --git a/changelog/7587.trivial.rst b/changelog/7587.trivial.rst new file mode 100644 index 00000000000..1477c97caef --- /dev/null +++ b/changelog/7587.trivial.rst @@ -0,0 +1 @@ +The dependency on the ``more-itertools`` package has been removed. diff --git a/setup.cfg b/setup.cfg index 31123f28e2e..518b6d41c93 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,6 @@ packages = install_requires = attrs>=17.4.0 iniconfig - more-itertools>=4.0.0 packaging pluggy>=0.12,<1.0 py>=1.8.2 diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 9521a7a17e8..d9f91874540 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1,6 +1,5 @@ import functools import inspect -import itertools import sys import warnings from collections import defaultdict @@ -1489,10 +1488,10 @@ def getfixtureinfo( else: argnames = () - usefixtures = itertools.chain.from_iterable( - mark.args for mark in node.iter_markers(name="usefixtures") + usefixtures = tuple( + arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args ) - initialnames = tuple(usefixtures) + argnames + initialnames = usefixtures + argnames fm = node.session._fixturemanager initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure( initialnames, node, ignore_args=self._get_direct_parametrize_args(node) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index e30471995e7..fb6c76852d2 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,11 +1,9 @@ -import inspect import math import pprint from collections.abc import Iterable from collections.abc import Mapping from collections.abc import Sized from decimal import Decimal -from itertools import filterfalse from numbers import Number from types import TracebackType from typing import Any @@ -18,8 +16,6 @@ from typing import TypeVar from typing import Union -from more_itertools.more import always_iterable - import _pytest._code from _pytest.compat import overload from _pytest.compat import STRING_TYPES @@ -30,9 +26,6 @@ from typing import Type -BASE_TYPE = (type, STRING_TYPES) - - def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: at_str = " at {}".format(at) if at else "" return TypeError( @@ -680,11 +673,16 @@ def raises( # noqa: F811 documentation for :ref:`the try statement `. """ __tracebackhide__ = True - for exc in filterfalse( - inspect.isclass, always_iterable(expected_exception, BASE_TYPE) - ): - msg = "exceptions must be derived from BaseException, not %s" - raise TypeError(msg % type(exc)) + + if isinstance(expected_exception, type): + excepted_exceptions = (expected_exception,) # type: Tuple[Type[_E], ...] + else: + excepted_exceptions = expected_exception + for exc in excepted_exceptions: + if not isinstance(exc, type) or not issubclass(exc, BaseException): + msg = "expected exception must be a BaseException type, not {}" + not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__ + raise TypeError(msg.format(not_a)) message = "DID NOT RAISE {}".format(expected_exception) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index ef9da50f3f8..cbca9ba465a 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -25,7 +25,6 @@ import attr import pluggy import py -from more_itertools import collapse import pytest from _pytest import nodes @@ -715,11 +714,14 @@ def pytest_sessionstart(self, session: "Session") -> None: self._write_report_lines_from_hooks(lines) def _write_report_lines_from_hooks( - self, lines: List[Union[str, List[str]]] + self, lines: Sequence[Union[str, Sequence[str]]] ) -> None: - lines.reverse() - for line in collapse(lines): - self.write_line(line) + for line_or_lines in reversed(lines): + if isinstance(line_or_lines, str): + self.write_line(line_or_lines) + else: + for line in line_or_lines: + self.write_line(line) def pytest_report_header(self, config: Config) -> List[str]: line = "rootdir: %s" % config.rootdir diff --git a/testing/python/raises.py b/testing/python/raises.py index 12d44495c99..46b200921e3 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -283,3 +283,22 @@ def test_raises_context_manager_with_kwargs(self): with pytest.raises(Exception, foo="bar"): # type: ignore[call-overload] pass assert "Unexpected keyword arguments" in str(excinfo.value) + + def test_expected_exception_is_not_a_baseexception(self) -> None: + with pytest.raises(TypeError) as excinfo: + with pytest.raises("hello"): # type: ignore[call-overload] + pass # pragma: no cover + assert "must be a BaseException type, not str" in str(excinfo.value) + + class NotAnException: + pass + + with pytest.raises(TypeError) as excinfo: + with pytest.raises(NotAnException): # type: ignore[type-var] + pass # pragma: no cover + assert "must be a BaseException type, not NotAnException" in str(excinfo.value) + + with pytest.raises(TypeError) as excinfo: + with pytest.raises(("hello", NotAnException)): # type: ignore[arg-type] + pass # pragma: no cover + assert "must be a BaseException type, not str" in str(excinfo.value) From 8d98de8f8aef70a68ec98c1dd7ed0291efb37429 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 31 Jul 2020 09:46:56 +0300 Subject: [PATCH 527/823] typing: set no_implicit_reexport In Python, if module A defines a name `name`, and module B does `import name from A`, then another module C can `import name from B`. Sometimes it is intentional -- module B is meant to "reexport" `name`. But sometimes it is just confusion/inconsistency on where `name` should be imported from. mypy has a flag `--no-implicit-reexport` which puts some order into this. A name can only be imported from a module if 1. The module defines the name 2. The module's `__all__` includes the name 3. The module imports the name as `from ... import .. as name`. This flag is included in mypy's `--strict` flag. I like this flag, but I realize it is a bit controversial, and in particular item 3 above is a bit unfriendly to contributors who don't know about it. So I didn't intend to add it to pytest. But while investigating issue 7589 I came upon mypy issue 8754 which causes `--no-implicit-reexport` to leak into installed libraries and causes some unexpected typing differences *in pytest* if the user uses this flag. Since the diff mostly makes sense, let's just conform to it. --- setup.cfg | 1 + src/_pytest/_code/__init__.py | 2 +- src/_pytest/compat.py | 6 +++--- src/_pytest/config/__init__.py | 4 ++-- src/_pytest/mark/__init__.py | 9 ++++++++- src/_pytest/python.py | 4 ++-- testing/python/collect.py | 4 ++-- testing/python/fixtures.py | 17 +++++++++-------- testing/python/integration.py | 6 +++--- testing/python/metafunc.py | 12 +++++++----- testing/test_mark.py | 2 +- 11 files changed, 39 insertions(+), 28 deletions(-) diff --git a/setup.cfg b/setup.cfg index 31123f28e2e..4a86e7ec194 100644 --- a/setup.cfg +++ b/setup.cfg @@ -104,3 +104,4 @@ strict_equality = True warn_redundant_casts = True warn_return_any = True warn_unused_configs = True +no_implicit_reexport = True diff --git a/src/_pytest/_code/__init__.py b/src/_pytest/_code/__init__.py index 136da31959e..511d0dde661 100644 --- a/src/_pytest/_code/__init__.py +++ b/src/_pytest/_code/__init__.py @@ -4,9 +4,9 @@ from .code import filter_traceback from .code import Frame from .code import getfslineno -from .code import getrawcode from .code import Traceback from .code import TracebackEntry +from .source import getrawcode from .source import Source __all__ = [ diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index cd7dca7197a..4ff59e1fb5e 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -14,7 +14,7 @@ from typing import Callable from typing import Generic from typing import Optional -from typing import overload +from typing import overload as overload from typing import Tuple from typing import TypeVar from typing import Union @@ -208,7 +208,7 @@ def nullcontext(): else: - from contextlib import nullcontext # noqa + from contextlib import nullcontext as nullcontext # noqa: F401 def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]: @@ -363,7 +363,7 @@ def overload(f): # noqa: F811 if sys.version_info >= (3, 8): - from functools import cached_property + from functools import cached_property as cached_property else: class cached_property(Generic[_S, _T]): diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index f4e0d5d0fab..188cccd1a65 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -35,8 +35,8 @@ import _pytest._code import _pytest.deprecated import _pytest.hookspec # the extension point definitions -from .exceptions import PrintHelp -from .exceptions import UsageError +from .exceptions import PrintHelp as PrintHelp +from .exceptions import UsageError as UsageError from .findpaths import determine_setup from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 5d71a772526..bc1dd1a709c 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -28,7 +28,14 @@ from _pytest.nodes import Item -__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"] +__all__ = [ + "MARK_GEN", + "Mark", + "MarkDecorator", + "MarkGenerator", + "ParameterSet", + "get_empty_parameterset_mark", +] old_mark_config_key = StoreKey[Optional[Config]]() diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e2b6aef9c44..50f03eadb15 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -32,6 +32,7 @@ from _pytest._code import filter_traceback from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo +from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter from _pytest._io.saferepr import saferepr from _pytest.compat import ascii_escaped @@ -66,7 +67,6 @@ from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import parts from _pytest.pathlib import visit -from _pytest.reports import TerminalRepr from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning @@ -581,7 +581,7 @@ def _importtestmodule(self): "Traceback:\n" "{traceback}".format(fspath=self.fspath, traceback=formatted_tb) ) from e - except _pytest.runner.Skipped as e: + except skip.Exception as e: if e.allow_module_level: raise raise self.CollectError( diff --git a/testing/python/collect.py b/testing/python/collect.py index f64a1462971..ed778c265ff 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1018,7 +1018,7 @@ def test_filter_traceback_generated_code(self) -> None: See: https://bitbucket.org/pytest-dev/py/issues/71 This fixes #995. """ - from _pytest.python import filter_traceback + from _pytest._code import filter_traceback try: ns = {} # type: Dict[str, Any] @@ -1038,7 +1038,7 @@ def test_filter_traceback_path_no_longer_valid(self, testdir) -> None: In this case, one of the files in the traceback no longer exists. This fixes #1133. """ - from _pytest.python import filter_traceback + from _pytest._code import filter_traceback testdir.syspathinsert() testdir.makepyfile( diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 119c7dedaf6..70370915b0e 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3,6 +3,7 @@ import pytest from _pytest import fixtures +from _pytest.compat import getfuncargnames from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest from _pytest.pathlib import Path @@ -15,22 +16,22 @@ def test_getfuncargnames_functions(): def f(): raise NotImplementedError() - assert not fixtures.getfuncargnames(f) + assert not getfuncargnames(f) def g(arg): raise NotImplementedError() - assert fixtures.getfuncargnames(g) == ("arg",) + assert getfuncargnames(g) == ("arg",) def h(arg1, arg2="hello"): raise NotImplementedError() - assert fixtures.getfuncargnames(h) == ("arg1",) + assert getfuncargnames(h) == ("arg1",) def j(arg1, arg2, arg3="hello"): raise NotImplementedError() - assert fixtures.getfuncargnames(j) == ("arg1", "arg2") + assert getfuncargnames(j) == ("arg1", "arg2") def test_getfuncargnames_methods(): @@ -40,7 +41,7 @@ class A: def f(self, arg1, arg2="hello"): raise NotImplementedError() - assert fixtures.getfuncargnames(A().f) == ("arg1",) + assert getfuncargnames(A().f) == ("arg1",) def test_getfuncargnames_staticmethod(): @@ -51,7 +52,7 @@ class A: def static(arg1, arg2, x=1): raise NotImplementedError() - assert fixtures.getfuncargnames(A.static, cls=A) == ("arg1", "arg2") + assert getfuncargnames(A.static, cls=A) == ("arg1", "arg2") def test_getfuncargnames_partial(): @@ -64,7 +65,7 @@ def check(arg1, arg2, i): class T: test_ok = functools.partial(check, i=2) - values = fixtures.getfuncargnames(T().test_ok, name="test_ok") + values = getfuncargnames(T().test_ok, name="test_ok") assert values == ("arg1", "arg2") @@ -78,7 +79,7 @@ def check(arg1, arg2, i): class T: test_ok = staticmethod(functools.partial(check, i=2)) - values = fixtures.getfuncargnames(T().test_ok, name="test_ok") + values = getfuncargnames(T().test_ok, name="test_ok") assert values == ("arg1", "arg2") diff --git a/testing/python/integration.py b/testing/python/integration.py index 537057484d0..854593a65c0 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -1,8 +1,8 @@ from typing import Any import pytest -from _pytest import python from _pytest import runner +from _pytest._code import getfslineno class TestOEJSKITSpecials: @@ -87,8 +87,8 @@ def wrap(f): def wrapped_func(x, y, z): pass - fs, lineno = python.getfslineno(wrapped_func) - fs2, lineno2 = python.getfslineno(wrap) + fs, lineno = getfslineno(wrapped_func) + fs2, lineno2 = getfslineno(wrap) assert lineno > lineno2, "getfslineno does not unwrap correctly" diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 4e6cfaf91ba..d254dd3fb33 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -19,6 +19,8 @@ import pytest from _pytest import fixtures from _pytest import python +from _pytest.compat import _format_args +from _pytest.compat import getfuncargnames from _pytest.outcomes import fail from _pytest.pytester import Testdir from _pytest.python import _idval @@ -41,7 +43,7 @@ class DefinitionMock(python.FunctionDefinition): obj = attr.ib() _nodeid = attr.ib() - names = fixtures.getfuncargnames(func) + names = getfuncargnames(func) fixtureinfo = FuncFixtureInfoMock(names) # type: Any definition = DefinitionMock._create(func, "mock::nodeid") # type: Any return python.Metafunc(definition, fixtureinfo, config) @@ -954,22 +956,22 @@ def test_format_args(self) -> None: def function1(): pass - assert fixtures._format_args(function1) == "()" + assert _format_args(function1) == "()" def function2(arg1): pass - assert fixtures._format_args(function2) == "(arg1)" + assert _format_args(function2) == "(arg1)" def function3(arg1, arg2="qwe"): pass - assert fixtures._format_args(function3) == "(arg1, arg2='qwe')" + assert _format_args(function3) == "(arg1, arg2='qwe')" def function4(arg1, *args, **kwargs): pass - assert fixtures._format_args(function4) == "(arg1, *args, **kwargs)" + assert _format_args(function4) == "(arg1, *args, **kwargs)" class TestMetafuncFunctional: diff --git a/testing/test_mark.py b/testing/test_mark.py index f35660093e7..f00c1330e65 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -4,8 +4,8 @@ import pytest from _pytest.config import ExitCode -from _pytest.mark import EMPTY_PARAMETERSET_OPTION from _pytest.mark import MarkGenerator as Mark +from _pytest.mark.structures import EMPTY_PARAMETERSET_OPTION from _pytest.nodes import Collector from _pytest.nodes import Node From 09265eb7c731a658fe27e1d8a5f640c42effcd90 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 31 Jul 2020 15:46:02 -0300 Subject: [PATCH 528/823] Configure setuptools_scm using pyproject.toml --- pyproject.toml | 5 ++++- setup.py | 7 +------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 493213d841e..737f5034679 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,11 +2,14 @@ requires = [ # sync with setup.py until we discard non-pep-517/518 "setuptools>=40.0", - "setuptools-scm", + "setuptools-scm>=3.4", "wheel", ] build-backend = "setuptools.build_meta" +[tool.setuptools_scm] +write_to = "src/_pytest/_version.py" + [tool.pytest.ini_options] minversion = "2.0" addopts = "-rfEX -p pytester --strict-markers" diff --git a/setup.py b/setup.py index 4475e30a71e..7f1a1763ca9 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,4 @@ from setuptools import setup - -def main(): - setup(use_scm_version={"write_to": "src/_pytest/_version.py"}) - - if __name__ == "__main__": - main() + setup() From 4f0793a4629a3ee5f34bcb3336c1390aaab29a76 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 31 Jul 2020 18:17:12 -0300 Subject: [PATCH 529/823] Require setuptools >=42 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 737f5034679..e98a028045f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = [ # sync with setup.py until we discard non-pep-517/518 - "setuptools>=40.0", + "setuptools>=42.0", "setuptools-scm>=3.4", "wheel", ] From d5a49100cf8581e5a225fea2524ae1e7039a8ecd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 Jul 2020 21:44:44 -0700 Subject: [PATCH 530/823] Try this maybe? --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e98a028045f..3d3d232f4b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ requires = [ # sync with setup.py until we discard non-pep-517/518 "setuptools>=42.0", - "setuptools-scm>=3.4", + "setuptools-scm[toml]>=3.4", "wheel", ] build-backend = "setuptools.build_meta" From 0242de4f5651818379bc0ff6326c97565a20a0f1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 14 Jul 2020 10:55:17 +0300 Subject: [PATCH 531/823] Format docstrings in a consistent style --- doc/en/reference.rst | 14 +- src/_pytest/_argcomplete.py | 48 +++-- src/_pytest/_code/code.py | 83 ++++---- src/_pytest/_code/source.py | 41 ++-- src/_pytest/_io/saferepr.py | 17 +- src/_pytest/_io/terminalwriter.py | 14 +- src/_pytest/assertion/__init__.py | 19 +- src/_pytest/assertion/rewrite.py | 36 ++-- src/_pytest/assertion/truncate.py | 14 +- src/_pytest/assertion/util.py | 16 +- src/_pytest/cacheprovider.py | 70 +++---- src/_pytest/capture.py | 99 ++++----- src/_pytest/compat.py | 74 ++++--- src/_pytest/config/__init__.py | 219 ++++++++++---------- src/_pytest/config/argparsing.py | 88 ++++---- src/_pytest/config/exceptions.py | 6 +- src/_pytest/config/findpaths.py | 20 +- src/_pytest/debugging.py | 18 +- src/_pytest/deprecated.py | 5 +- src/_pytest/doctest.py | 87 +++----- src/_pytest/faulthandler.py | 6 +- src/_pytest/fixtures.py | 334 +++++++++++++++--------------- src/_pytest/freeze_support.py | 15 +- src/_pytest/helpconfig.py | 9 +- src/_pytest/hookspec.py | 228 ++++++++++---------- src/_pytest/junitxml.py | 70 +++---- src/_pytest/logging.py | 136 ++++++------ src/_pytest/main.py | 40 ++-- src/_pytest/mark/__init__.py | 16 +- src/_pytest/mark/expression.py | 10 +- src/_pytest/mark/structures.py | 31 ++- src/_pytest/monkeypatch.py | 90 ++++---- src/_pytest/nodes.py | 153 +++++++------- src/_pytest/nose.py | 12 +- src/_pytest/outcomes.py | 84 ++++---- src/_pytest/pastebin.py | 21 +- src/_pytest/pathlib.py | 101 +++++---- src/_pytest/pytester.py | 260 +++++++++++------------ src/_pytest/python.py | 240 ++++++++++----------- src/_pytest/python_api.py | 79 +++---- src/_pytest/recwarn.py | 2 +- src/_pytest/reports.py | 94 ++++----- src/_pytest/resultlog.py | 6 +- src/_pytest/runner.py | 41 ++-- src/_pytest/skipping.py | 6 +- src/_pytest/store.py | 6 +- src/_pytest/terminal.py | 68 +++--- src/_pytest/timing.py | 3 +- src/_pytest/tmpdir.py | 66 +++--- src/_pytest/unittest.py | 42 ++-- src/_pytest/warning_types.py | 2 +- src/_pytest/warnings.py | 25 +-- src/pytest/__init__.py | 4 +- src/pytest/__main__.py | 4 +- testing/test_tmpdir.py | 6 +- 55 files changed, 1601 insertions(+), 1697 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 775dd556a8a..f4a68f16046 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -182,7 +182,7 @@ Mark a test function as using the given fixture names. .. py:function:: pytest.mark.usefixtures(*names) - :param args: the names of the fixture to use, as strings + :param args: The names of the fixture to use, as strings. .. note:: @@ -209,8 +209,10 @@ Marks a test function as *expected to fail*. Condition for marking the test function as xfail (``True/False`` or a :ref:`condition string `). If a bool, you also have to specify ``reason`` (see :ref:`condition string `). - :keyword str reason: Reason why the test function is marked as xfail. - :keyword Exception raises: Exception subclass expected to be raised by the test function; other exceptions will fail the test. + :keyword str reason: + Reason why the test function is marked as xfail. + :keyword Type[Exception] raises: + Exception subclass expected to be raised by the test function; other exceptions will fail the test. :keyword bool run: If the test function should actually be executed. If ``False``, the function will always xfail and will not be executed (useful if a function is segfaulting). @@ -224,7 +226,7 @@ Marks a test function as *expected to fail*. a new release of a library fixes a known bug). -custom marks +Custom marks ~~~~~~~~~~~~ Marks are created dynamically using the factory object ``pytest.mark`` and applied as a decorator. @@ -473,7 +475,7 @@ caplog .. autofunction:: _pytest.logging.caplog() :no-auto-options: - This returns a :class:`_pytest.logging.LogCaptureFixture` instance. + Returns a :class:`_pytest.logging.LogCaptureFixture` instance. .. autoclass:: _pytest.logging.LogCaptureFixture :members: @@ -491,7 +493,7 @@ monkeypatch .. autofunction:: _pytest.monkeypatch.monkeypatch() :no-auto-options: - This returns a :class:`MonkeyPatch` instance. + Returns a :class:`MonkeyPatch` instance. .. autoclass:: _pytest.monkeypatch.MonkeyPatch :members: diff --git a/src/_pytest/_argcomplete.py b/src/_pytest/_argcomplete.py index 7ca216ecf96..3dbdf9318be 100644 --- a/src/_pytest/_argcomplete.py +++ b/src/_pytest/_argcomplete.py @@ -1,7 +1,8 @@ -"""allow bash-completion for argparse with argcomplete if installed -needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail +"""Allow bash-completion for argparse with argcomplete if installed. + +Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail to find the magic string, so _ARGCOMPLETE env. var is never set, and -this does not need special code. +this does not need special code). Function try_argcomplete(parser) should be called directly before the call to ArgumentParser.parse_args(). @@ -10,8 +11,7 @@ arguments specification, in order to get "dirname/" after "dirn" instead of the default "dirname ": - optparser.add_argument(Config._file_or_dir, nargs='*' - ).completer=filescompleter + optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter Other, application specific, completers should go in the file doing the add_argument calls as they need to be specified as .completer @@ -20,35 +20,43 @@ SPEEDUP ======= + The generic argcomplete script for bash-completion -(/etc/bash_completion.d/python-argcomplete.sh ) +(/etc/bash_completion.d/python-argcomplete.sh) uses a python program to determine startup script generated by pip. You can speed up completion somewhat by changing this script to include # PYTHON_ARGCOMPLETE_OK so the the python-argcomplete-check-easy-install-script does not need to be called to find the entry point of the code and see if that is -marked with PYTHON_ARGCOMPLETE_OK +marked with PYTHON_ARGCOMPLETE_OK. INSTALL/DEBUGGING ================= + To include this support in another application that has setup.py generated scripts: -- add the line: + +- Add the line: # PYTHON_ARGCOMPLETE_OK - near the top of the main python entry point -- include in the file calling parse_args(): + near the top of the main python entry point. + +- Include in the file calling parse_args(): from _argcomplete import try_argcomplete, filescompleter - , call try_argcomplete just before parse_args(), and optionally add - filescompleter to the positional arguments' add_argument() + Call try_argcomplete just before parse_args(), and optionally add + filescompleter to the positional arguments' add_argument(). + If things do not work right away: -- switch on argcomplete debugging with (also helpful when doing custom + +- Switch on argcomplete debugging with (also helpful when doing custom completers): export _ARC_DEBUG=1 -- run: + +- Run: python-argcomplete-check-easy-install-script $(which appname) echo $? - will echo 0 if the magic line has been found, 1 if not -- sometimes it helps to find early on errors using: + will echo 0 if the magic line has been found, 1 if not. + +- Sometimes it helps to find early on errors using: _ARGCOMPLETE=1 _ARC_DEBUG=1 appname which should throw a KeyError: 'COMPLINE' (which is properly set by the global argcomplete script). @@ -63,13 +71,13 @@ class FastFilesCompleter: - "Fast file completer class" + """Fast file completer class.""" def __init__(self, directories: bool = True) -> None: self.directories = directories def __call__(self, prefix: str, **kwargs: Any) -> List[str]: - """only called on non option completions""" + # Only called on non option completions. if os.path.sep in prefix[1:]: prefix_dir = len(os.path.dirname(prefix) + os.path.sep) else: @@ -77,7 +85,7 @@ def __call__(self, prefix: str, **kwargs: Any) -> List[str]: completion = [] globbed = [] if "*" not in prefix and "?" not in prefix: - # we are on unix, otherwise no bash + # We are on unix, otherwise no bash. if not prefix or prefix[-1] == os.path.sep: globbed.extend(glob(prefix + ".*")) prefix += "*" @@ -85,7 +93,7 @@ def __call__(self, prefix: str, **kwargs: Any) -> List[str]: for x in sorted(globbed): if os.path.isdir(x): x += "/" - # append stripping the prefix (like bash, not like compgen) + # Append stripping the prefix (like bash, not like compgen). completion.append(x[prefix_dir:]) return completion diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 219ebb68ff5..e0aadd724ca 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -71,9 +71,8 @@ def __eq__(self, other): @property def path(self) -> Union[py.path.local, str]: - """Return a path object pointing to source code (or a str in case - of OSError / non-existing file). - """ + """Return a path object pointing to source code, or an ``str`` in + case of ``OSError`` / non-existing file.""" if not self.raw.co_filename: return "" try: @@ -420,15 +419,16 @@ def from_exc_info( exc_info: Tuple["Type[_E]", "_E", TracebackType], exprinfo: Optional[str] = None, ) -> "ExceptionInfo[_E]": - """Returns an ExceptionInfo for an existing exc_info tuple. + """Return an ExceptionInfo for an existing exc_info tuple. .. warning:: Experimental API - :param exprinfo: a text string helping to determine if we should - strip ``AssertionError`` from the output, defaults - to the exception message/``__str__()`` + :param exprinfo: + A text string helping to determine if we should strip + ``AssertionError`` from the output. Defaults to the exception + message/``__str__()``. """ _striptext = "" if exprinfo is None and isinstance(exc_info[1], AssertionError): @@ -444,15 +444,16 @@ def from_exc_info( def from_current( cls, exprinfo: Optional[str] = None ) -> "ExceptionInfo[BaseException]": - """Returns an ExceptionInfo matching the current traceback. + """Return an ExceptionInfo matching the current traceback. .. warning:: Experimental API - :param exprinfo: a text string helping to determine if we should - strip ``AssertionError`` from the output, defaults - to the exception message/``__str__()`` + :param exprinfo: + A text string helping to determine if we should strip + ``AssertionError`` from the output. Defaults to the exception + message/``__str__()``. """ tup = sys.exc_info() assert tup[0] is not None, "no current exception" @@ -467,7 +468,7 @@ def for_later(cls) -> "ExceptionInfo[_E]": return cls(None) def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None: - """fill an unfilled ExceptionInfo created with for_later()""" + """Fill an unfilled ExceptionInfo created with ``for_later()``.""" assert self._excinfo is None, "ExceptionInfo was already filled" self._excinfo = exc_info @@ -568,7 +569,8 @@ def getrepr( Show locals per traceback entry. Ignored if ``style=="native"``. - :param str style: long|short|no|native|value traceback style + :param str style: + long|short|no|native|value traceback style. :param bool abspath: If paths should be changed to absolute or left unchanged. @@ -583,7 +585,8 @@ def getrepr( :param bool truncate_locals: With ``showlocals==True``, make sure locals can be safely represented as strings. - :param bool chain: if chained exceptions in Python 3 should be shown. + :param bool chain: + If chained exceptions in Python 3 should be shown. .. versionchanged:: 3.9 @@ -643,7 +646,7 @@ class FormattedExcinfo: astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False) def _getindent(self, source: "Source") -> int: - # figure out indent for given source + # Figure out indent for the given source. try: s = str(source.getstatement(len(source) - 1)) except KeyboardInterrupt: @@ -704,7 +707,7 @@ def get_exconly( ) -> List[str]: lines = [] indentstr = " " * indent - # get the real exception information out + # Get the real exception information out. exlines = excinfo.exconly(tryshort=True).split("\n") failindent = self.fail_marker + indentstr[1:] for line in exlines: @@ -730,8 +733,7 @@ def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]: str_repr = saferepr(value) else: str_repr = safeformat(value) - # if len(str_repr) < 70 or not isinstance(value, - # (list, tuple, dict)): + # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)): lines.append("{:<10} = {}".format(name, str_repr)) # else: # self._line("%-10s =\\" % (name,)) @@ -809,16 +811,17 @@ def repr_traceback(self, excinfo: ExceptionInfo) -> "ReprTraceback": def _truncate_recursive_traceback( self, traceback: Traceback ) -> Tuple[Traceback, Optional[str]]: - """ - Truncate the given recursive traceback trying to find the starting point - of the recursion. + """Truncate the given recursive traceback trying to find the starting + point of the recursion. - The detection is done by going through each traceback entry and finding the - point in which the locals of the frame are equal to the locals of a previous frame (see ``recursionindex()``. + The detection is done by going through each traceback entry and + finding the point in which the locals of the frame are equal to the + locals of a previous frame (see ``recursionindex()``). - Handle the situation where the recursion process might raise an exception (for example - comparing numpy arrays using equality raises a TypeError), in which case we do our best to - warn the user of the error and show a limited traceback. + Handle the situation where the recursion process might raise an + exception (for example comparing numpy arrays using equality raises a + TypeError), in which case we do our best to warn the user of the + error and show a limited traceback. """ try: recursionindex = traceback.recursionindex() @@ -863,8 +866,8 @@ def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr": excinfo_._getreprcrash() if self.style != "value" else None ) # type: Optional[ReprFileLocation] else: - # fallback to native repr if the exception doesn't have a traceback: - # ExceptionInfo objects require a full traceback to work + # Fallback to native repr if the exception doesn't have a traceback: + # ExceptionInfo objects require a full traceback to work. reprtraceback = ReprTracebackNative( traceback.format_exception(type(e), e, None) ) @@ -915,7 +918,7 @@ def toterminal(self, tw: TerminalWriter) -> None: # This class is abstract -- only subclasses are instantiated. @attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ExceptionRepr(TerminalRepr): - # Provided by in subclasses. + # Provided by subclasses. reprcrash = None # type: Optional[ReprFileLocation] reprtraceback = None # type: ReprTraceback @@ -942,7 +945,7 @@ class ExceptionChainRepr(ExceptionRepr): def __attrs_post_init__(self) -> None: super().__attrs_post_init__() # reprcrash and reprtraceback of the outermost (the newest) exception - # in the chain + # in the chain. self.reprtraceback = self.chain[-1][0] self.reprcrash = self.chain[-1][1] @@ -974,7 +977,7 @@ class ReprTraceback(TerminalRepr): entrysep = "_ " def toterminal(self, tw: TerminalWriter) -> None: - # the entries might have different styles + # The entries might have different styles. for i, entry in enumerate(self.reprentries): if entry.style == "long": tw.line("") @@ -1017,7 +1020,7 @@ class ReprEntry(TerminalRepr): style = attr.ib(type="_TracebackStyle") def _write_entry_lines(self, tw: TerminalWriter) -> None: - """Writes the source code portions of a list of traceback entries with syntax highlighting. + """Write the source code portions of a list of traceback entries with syntax highlighting. Usually entries are lines like these: @@ -1099,8 +1102,8 @@ class ReprFileLocation(TerminalRepr): message = attr.ib(type=str) def toterminal(self, tw: TerminalWriter) -> None: - # filename and lineno output for each entry, - # using an output format that most editors understand + # Filename and lineno output for each entry, using an output format + # that most editors understand. msg = self.message i = msg.find("\n") if i != -1: @@ -1175,10 +1178,10 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: return code.path, code.firstlineno -# relative paths that we use to filter traceback entries from appearing to the user; -# see filter_traceback +# Relative paths that we use to filter traceback entries from appearing to the user; +# see filter_traceback. # note: if we need to add more paths than what we have now we should probably use a list -# for better maintenance +# for better maintenance. _PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc")) # pluggy is either a package or a single module depending on the version @@ -1197,14 +1200,14 @@ def filter_traceback(entry: TracebackEntry) -> bool: * internal traceback from pytest or its internal libraries, py and pluggy. """ # entry.path might sometimes return a str object when the entry - # points to dynamically generated code - # see https://bitbucket.org/pytest-dev/py/issues/71 + # points to dynamically generated code. + # See https://bitbucket.org/pytest-dev/py/issues/71. raw_filename = entry.frame.code.raw.co_filename is_generated = "<" in raw_filename and ">" in raw_filename if is_generated: return False # entry.path might point to a non-existing file, in which case it will - # also return a str object. see #1133 + # also return a str object. See #1133. p = py.path.local(entry.path) return ( not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 65560be2a5e..8338014ae89 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -67,9 +67,7 @@ def __len__(self) -> int: return len(self.lines) def strip(self) -> "Source": - """ return new source object with trailing - and leading blank lines removed. - """ + """Return new Source object with trailing and leading blank lines removed.""" start, end = 0, len(self) while start < end and not self.lines[start].strip(): start += 1 @@ -80,31 +78,28 @@ def strip(self) -> "Source": return source def indent(self, indent: str = " " * 4) -> "Source": - """ return a copy of the source object with - all lines indented by the given indent-string. - """ + """Return a copy of the source object with all lines indented by the + given indent-string.""" newsource = Source() newsource.lines = [(indent + line) for line in self.lines] return newsource def getstatement(self, lineno: int) -> "Source": - """ return Source statement which contains the - given linenumber (counted from 0). - """ + """Return Source statement which contains the given linenumber + (counted from 0).""" start, end = self.getstatementrange(lineno) return self[start:end] def getstatementrange(self, lineno: int) -> Tuple[int, int]: - """ return (start, end) tuple which spans the minimal - statement region which containing the given lineno. - """ + """Return (start, end) tuple which spans the minimal statement region + which containing the given lineno.""" if not (0 <= lineno < len(self)): raise IndexError("lineno out of range") ast, start, end = getstatementrange_ast(lineno, self) return start, end def deindent(self) -> "Source": - """return a new source object deindented.""" + """Return a new Source object deindented.""" newsource = Source() newsource.lines[:] = deindent(self.lines) return newsource @@ -129,7 +124,7 @@ def findsource(obj) -> Tuple[Optional[Source], int]: def getrawcode(obj, trycall: bool = True): - """ return code object for given function. """ + """Return code object for given function.""" try: return obj.__code__ except AttributeError: @@ -148,8 +143,8 @@ def deindent(lines: Iterable[str]) -> List[str]: def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]: - # flatten all statements and except handlers into one lineno-list - # AST's line numbers start indexing at 1 + # Flatten all statements and except handlers into one lineno-list. + # AST's line numbers start indexing at 1. values = [] # type: List[int] for x in ast.walk(node): if isinstance(x, (ast.stmt, ast.ExceptHandler)): @@ -157,7 +152,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i for name in ("finalbody", "orelse"): val = getattr(x, name, None) # type: Optional[List[ast.stmt]] if val: - # treat the finally/orelse part as its own statement + # Treat the finally/orelse part as its own statement. values.append(val[0].lineno - 1 - 1) values.sort() insert_index = bisect_right(values, lineno) @@ -178,13 +173,13 @@ def getstatementrange_ast( if astnode is None: content = str(source) # See #4260: - # don't produce duplicate warnings when compiling source to find ast + # Don't produce duplicate warnings when compiling source to find AST. with warnings.catch_warnings(): warnings.simplefilter("ignore") astnode = ast.parse(content, "source", "exec") start, end = get_statement_startend2(lineno, astnode) - # we need to correct the end: + # We need to correct the end: # - ast-parsing strips comments # - there might be empty lines # - we might have lesser indented code blocks at the end @@ -192,10 +187,10 @@ def getstatementrange_ast( end = len(source.lines) if end > start + 1: - # make sure we don't span differently indented code blocks - # by using the BlockFinder helper used which inspect.getsource() uses itself + # Make sure we don't span differently indented code blocks + # by using the BlockFinder helper used which inspect.getsource() uses itself. block_finder = inspect.BlockFinder() - # if we start with an indented line, put blockfinder to "started" mode + # If we start with an indented line, put blockfinder to "started" mode. block_finder.started = source.lines[start][0].isspace() it = ((x + "\n") for x in source.lines[start:end]) try: @@ -206,7 +201,7 @@ def getstatementrange_ast( except Exception: pass - # the end might still point to a comment or empty line, correct it + # The end might still point to a comment or empty line, correct it. while end: line = source.lines[end - 1].lstrip() if line.startswith("#") or not line: diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 823b8d71942..9a4975f61ad 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -36,9 +36,8 @@ def _ellipsize(s: str, maxsize: int) -> str: class SafeRepr(reprlib.Repr): - """subclass of repr.Repr that limits the resulting size of repr() - and includes information on exceptions raised during the call. - """ + """repr.Repr that limits the resulting size of repr() and includes + information on exceptions raised during the call.""" def __init__(self, maxsize: int) -> None: super().__init__() @@ -65,7 +64,8 @@ def repr_instance(self, x: object, level: int) -> str: def safeformat(obj: object) -> str: - """return a pretty printed string for the given object. + """Return a pretty printed string for the given object. + Failing __repr__ functions of user instances will be represented with a short exception info. """ @@ -76,11 +76,14 @@ def safeformat(obj: object) -> str: def saferepr(obj: object, maxsize: int = 240) -> str: - """return a size-limited safe repr-string for the given object. + """Return a size-limited safe repr-string for the given object. + Failing __repr__ functions of user instances will be represented with a short exception info and 'saferepr' generally takes - care to never raise exceptions itself. This function is a wrapper - around the Repr/reprlib functionality of the standard 2.6 lib. + care to never raise exceptions itself. + + This function is a wrapper around the Repr/reprlib functionality of the + standard 2.6 lib. """ return SafeRepr(maxsize).repr(obj) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 5ffc550db28..0afe4a0eda4 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -111,13 +111,13 @@ def sep( ) -> None: if fullwidth is None: fullwidth = self.fullwidth - # the goal is to have the line be as long as possible - # under the condition that len(line) <= fullwidth + # The goal is to have the line be as long as possible + # under the condition that len(line) <= fullwidth. if sys.platform == "win32": - # if we print in the last column on windows we are on a + # If we print in the last column on windows we are on a # new line but there is no way to verify/neutralize this - # (we may not know the exact line width) - # so let's be defensive to avoid empty lines in the output + # (we may not know the exact line width). + # So let's be defensive to avoid empty lines in the output. fullwidth -= 1 if title is not None: # we want 2 + 2*len(fill) + len(title) <= fullwidth @@ -131,9 +131,9 @@ def sep( # we want len(sepchar)*N <= fullwidth # i.e. N <= fullwidth // len(sepchar) line = sepchar * (fullwidth // len(sepchar)) - # in some situations there is room for an extra sepchar at the right, + # In some situations there is room for an extra sepchar at the right, # in particular if we consider that with a sepchar like "_ " the - # trailing space is not important at the end of the line + # trailing space is not important at the end of the line. if len(line) + len(sepchar.rstrip()) <= fullwidth: line += sepchar.rstrip() diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 64d2267e70a..bf9dadf4b00 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -1,6 +1,4 @@ -""" -support for presenting detailed information in failing assertions. -""" +"""Support for presenting detailed information in failing assertions.""" import sys from typing import Any from typing import Generator @@ -55,7 +53,7 @@ def register_assert_rewrite(*names: str) -> None: actually imported, usually in your __init__.py if you are a plugin using a package. - :raise TypeError: if the given module names are not strings. + :raises TypeError: If the given module names are not strings. """ for name in names: if not isinstance(name, str): @@ -105,9 +103,9 @@ def undo() -> None: def pytest_collection(session: "Session") -> None: - # this hook is only called when test modules are collected + # This hook is only called when test modules are collected # so for example not in the master process of pytest-xdist - # (which does not collect test modules) + # (which does not collect test modules). assertstate = session.config._store.get(assertstate_key, None) if assertstate: if assertstate.hook is not None: @@ -116,18 +114,17 @@ def pytest_collection(session: "Session") -> None: @hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: - """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks + """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks. - The rewrite module will use util._reprcompare if - it exists to use custom reporting via the - pytest_assertrepr_compare hook. This sets up this custom + The rewrite module will use util._reprcompare if it exists to use custom + reporting via the pytest_assertrepr_compare hook. This sets up this custom comparison for the test. """ ihook = item.ihook def callbinrepr(op, left: object, right: object) -> Optional[str]: - """Call the pytest_assertrepr_compare hook and prepare the result + """Call the pytest_assertrepr_compare hook and prepare the result. This uses the first result from the hook and then ensures the following: diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index e77b1b0b861..730d5382ad2 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -1,4 +1,4 @@ -"""Rewrite assertion AST to produce nice error messages""" +"""Rewrite assertion AST to produce nice error messages.""" import ast import errno import functools @@ -170,7 +170,7 @@ def exec_module(self, module: types.ModuleType) -> None: exec(co, module.__dict__) def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool: - """This is a fast way to get out of rewriting modules. + """A fast way to get out of rewriting modules. Profiling has shown that the call to PathFinder.find_spec (inside of the find_spec from this class) is a major slowdown, so, this method @@ -350,7 +350,7 @@ def _write_pyc( def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]: - """read and rewrite *fn* and return the code object.""" + """Read and rewrite *fn* and return the code object.""" fn_ = fspath(fn) stat = os.stat(fn_) with open(fn_, "rb") as f: @@ -411,7 +411,7 @@ def rewrite_asserts( def _saferepr(obj: object) -> str: - """Get a safe repr of an object for assertion error messages. + r"""Get a safe repr of an object for assertion error messages. The assertion formatting (util.format_explanation()) requires newlines to be escaped since they are a special character for it. @@ -419,18 +419,16 @@ def _saferepr(obj: object) -> str: custom repr it is possible to contain one of the special escape sequences, especially '\n{' and '\n}' are likely to be present in JSON reprs. - """ return saferepr(obj).replace("\n", "\\n") def _format_assertmsg(obj: object) -> str: - """Format the custom assertion message given. + r"""Format the custom assertion message given. For strings this simply replaces newlines with '\n~' so that util.format_explanation() will preserve them instead of escaping newlines. For other objects saferepr() is used first. - """ # reprlib appears to have a bug which means that if a string # contains a newline it gets escaped, however if an object has a @@ -491,8 +489,8 @@ def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None: def _check_if_assertion_pass_impl() -> bool: - """Checks if any plugins implement the pytest_assertion_pass hook - in order not to generate explanation unecessarily (might be expensive)""" + """Check if any plugins implement the pytest_assertion_pass hook + in order not to generate explanation unecessarily (might be expensive).""" return True if util._assertion_pass else False @@ -541,7 +539,7 @@ def _fix(node, lineno, col_offset): def _get_assertion_exprs(src: bytes) -> Dict[int, str]: - """Returns a mapping from {lineno: "assertion test expression"}""" + """Return a mapping from {lineno: "assertion test expression"}.""" ret = {} # type: Dict[int, str] depth = 0 @@ -645,7 +643,6 @@ class AssertionRewriter(ast.NodeVisitor): This state is reset on every new assert statement visited and used by the other visitors. - """ def __init__( @@ -770,7 +767,6 @@ def explanation_param(self, expr: ast.expr) -> str: current formatting context, e.g. ``%(py0)s``. The placeholder and expr are placed in the current format context so that it can be used on the next call to .pop_format_context(). - """ specifier = "py" + str(next(self.variable_counter)) self.explanation_specifiers[specifier] = expr @@ -785,7 +781,6 @@ def push_format_context(self) -> None: .explanation_param(). Finally .pop_format_context() is used to format a string of %-formatted values as added by .explanation_param(). - """ self.explanation_specifiers = {} # type: Dict[str, ast.expr] self.stack.append(self.explanation_specifiers) @@ -797,7 +792,6 @@ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name: the %-placeholders created by .explanation_param(). This will add the required code to format said string to .expl_stmts and return the ast.Name instance of the formatted string. - """ current = self.stack.pop() if self.stack: @@ -824,7 +818,6 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]: intermediate values and replace it with an if statement which raises an assertion error with a detailed explanation in case the expression is false. - """ if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1: from _pytest.warning_types import PytestAssertRewriteWarning @@ -994,9 +987,6 @@ def visit_BinOp(self, binop: ast.BinOp) -> Tuple[ast.Name, str]: return res, explanation def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]: - """ - visit `ast.Call` nodes - """ new_func, func_expl = self.visit(call.func) arg_expls = [] new_args = [] @@ -1021,7 +1011,7 @@ def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]: return res, outer_expl def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]: - # From Python 3.5, a Starred node can appear in a function call + # From Python 3.5, a Starred node can appear in a function call. res, expl = self.visit(starred.value) new_starred = ast.Starred(res, starred.ctx) return new_starred, "*" + expl @@ -1076,8 +1066,10 @@ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: def try_makedirs(cache_dir: Path) -> bool: - """Attempts to create the given directory and sub-directories exist, returns True if - successful or it already exists""" + """Attempt to create the given directory and sub-directories exist. + + Returns True if successful or if it already exists. + """ try: os.makedirs(fspath(cache_dir), exist_ok=True) except (FileNotFoundError, NotADirectoryError, FileExistsError): @@ -1096,7 +1088,7 @@ def try_makedirs(cache_dir: Path) -> bool: def get_cache_dir(file_path: Path) -> Path: - """Returns the cache directory to write .pyc files for the given .py file 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: # given: # prefix = '/tmp/pycs' diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index fb2bf9c8e35..c572cc74461 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -1,5 +1,4 @@ -""" -Utilities for truncating assertion output. +"""Utilities for truncating assertion output. Current default behaviour is to truncate assertion explanations at ~8 terminal lines, unless running in "-vv" mode or running on CI. @@ -19,18 +18,14 @@ def truncate_if_required( explanation: List[str], item: Item, max_length: Optional[int] = None ) -> List[str]: - """ - Truncate this assertion explanation if the given test item is eligible. - """ + """Truncate this assertion explanation if the given test item is eligible.""" if _should_truncate_item(item): return _truncate_explanation(explanation) return explanation def _should_truncate_item(item: Item) -> bool: - """ - Whether or not this test item is eligible for truncation. - """ + """Whether or not this test item is eligible for truncation.""" verbose = item.config.option.verbose return verbose < 2 and not _running_on_ci() @@ -46,8 +41,7 @@ def _truncate_explanation( max_lines: Optional[int] = None, max_chars: Optional[int] = None, ) -> List[str]: - """ - Truncate given list of strings that makes up the assertion explanation. + """Truncate given list of strings that makes up the assertion explanation. Truncates to either 8 lines, or 640 characters - whichever the input reaches first. The remaining lines will be replaced by a usage message. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 554aec77fa9..e80e476c84a 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -1,4 +1,4 @@ -"""Utilities for assertion debugging""" +"""Utilities for assertion debugging.""" import collections.abc import pprint from typing import AbstractSet @@ -30,7 +30,7 @@ def format_explanation(explanation: str) -> str: - """This formats an explanation + r"""Format an explanation. Normally all embedded newlines are escaped, however there are three exceptions: \n{, \n} and \n~. The first two are intended @@ -45,7 +45,7 @@ def format_explanation(explanation: str) -> str: def _split_explanation(explanation: str) -> List[str]: - """Return a list of individual lines in the explanation + r"""Return a list of individual lines in the explanation. This will return a list of lines split on '\n{', '\n}' and '\n~'. Any other newlines will be escaped and appear in the line as the @@ -62,11 +62,11 @@ def _split_explanation(explanation: str) -> List[str]: def _format_lines(lines: Sequence[str]) -> List[str]: - """Format the individual lines + """Format the individual lines. - This will replace the '{', '}' and '~' characters of our mini - formatting language with the proper 'where ...', 'and ...' and ' + - ...' text, taking care of indentation along the way. + This will replace the '{', '}' and '~' characters of our mini formatting + language with the proper 'where ...', 'and ...' and ' + ...' text, taking + care of indentation along the way. Return a list of formatted lines. """ @@ -129,7 +129,7 @@ def isiterable(obj: Any) -> bool: def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]: - """Return specialised explanations for some operators/operands""" + """Return specialised explanations for some operators/operands.""" verbose = config.getoption("verbose") if verbose > 1: left_repr = safeformat(left) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index de7ee914980..41c2582712c 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -1,9 +1,6 @@ -""" -merged implementation of the cache provider - -the name cache was not chosen to ensure pluggy automatically -ignores the external pytest-cache -""" +"""Implementation of the cache provider.""" +# This plugin was not named "cache" to avoid conflicts with the external +# pytest-cache version. import json import os from typing import Dict @@ -73,7 +70,7 @@ def for_config(cls, config: Config) -> "Cache": @classmethod def clear_cache(cls, cachedir: Path) -> None: - """Clears the sub-directories used to hold cached directories and values.""" + """Clear the sub-directories used to hold cached directories and values.""" for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES): d = cachedir / prefix if d.is_dir(): @@ -94,14 +91,16 @@ def warn(self, fmt: str, **args: object) -> None: ) def makedir(self, name: str) -> py.path.local: - """ return a directory path object with the given name. If the - directory does not yet exist, it will be created. You can use it - to manage files likes e. g. store/retrieve database - dumps across test sessions. - - :param name: must be a string not containing a ``/`` separator. - Make sure the name contains your plugin or application - identifiers to prevent clashes with other cache users. + """Return a directory path object with the given name. + + If the directory does not yet exist, it will be created. You can use + it to manage files to e.g. store/retrieve database dumps across test + sessions. + + :param name: + Must be a string not containing a ``/`` separator. + Make sure the name contains your plugin or application + identifiers to prevent clashes with other cache users. """ path = Path(name) if len(path.parts) > 1: @@ -114,15 +113,16 @@ def _getvaluepath(self, key: str) -> Path: return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key)) def get(self, key: str, default): - """ return cached value for the given key. If no value - was yet cached or the value cannot be read, the specified - default is returned. + """Return the cached value for the given key. - :param key: must be a ``/`` separated value. Usually the first - name is the name of your plugin or your application. - :param default: must be provided in case of a cache-miss or - invalid cache values. + If no value was yet cached or the value cannot be read, the specified + default is returned. + :param key: + Must be a ``/`` separated value. Usually the first + name is the name of your plugin or your application. + :param default: + The value to return in case of a cache-miss or invalid cache value. """ path = self._getvaluepath(key) try: @@ -132,13 +132,14 @@ def get(self, key: str, default): return default def set(self, key: str, value: object) -> None: - """ save value for the given key. - - :param key: must be a ``/`` separated value. Usually the first - name is the name of your plugin or your application. - :param value: must be of any combination of basic - python types, including nested types - like e. g. lists of dictionaries. + """Save value for the given key. + + :param key: + Must be a ``/`` separated value. Usually the first + name is the name of your plugin or your application. + :param value: + Must be of any combination of basic python types, + including nested types like lists of dictionaries. """ path = self._getvaluepath(key) try: @@ -241,7 +242,7 @@ def pytest_make_collect_report( class LFPlugin: - """ Plugin which implements the --lf (run last-failing) option """ + """Plugin which implements the --lf (run last-failing) option.""" def __init__(self, config: Config) -> None: self.config = config @@ -262,7 +263,7 @@ def __init__(self, config: Config) -> None: ) def get_last_failed_paths(self) -> Set[Path]: - """Returns a set with all Paths()s of the previously failed nodeids.""" + """Return a set with all Paths()s of the previously failed nodeids.""" rootpath = Path(str(self.config.rootdir)) result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed} return {x for x in result if x.exists()} @@ -351,7 +352,7 @@ def pytest_sessionfinish(self, session: Session) -> None: class NFPlugin: - """ Plugin which implements the --nf (run new-first) option """ + """Plugin which implements the --nf (run new-first) option.""" def __init__(self, config: Config) -> None: self.config = config @@ -471,13 +472,12 @@ def pytest_configure(config: Config) -> None: @pytest.fixture def cache(request: FixtureRequest) -> Cache: - """ - Return a cache object that can persist state between testing sessions. + """Return a cache object that can persist state between testing sessions. cache.get(key, default) cache.set(key, value) - Keys must be a ``/`` separated value, where the first part is usually the + Keys must be ``/`` separated strings, where the first part is usually the name of your plugin or application to avoid clashes with other cache users. Values can be any object handled by the json stdlib module. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index f538b67eceb..90f5f9f3f11 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -1,7 +1,4 @@ -""" -per-test stdout/stderr capturing mechanism. - -""" +"""Per-test stdout/stderr capturing mechanism.""" import collections import contextlib import io @@ -49,8 +46,7 @@ def pytest_addoption(parser: Parser) -> None: def _colorama_workaround() -> None: - """ - Ensure colorama is imported so that it attaches to the correct stdio + """Ensure colorama is imported so that it attaches to the correct stdio handles on Windows. colorama uses the terminal on import time. So if something does the @@ -65,8 +61,7 @@ def _colorama_workaround() -> None: def _readline_workaround() -> None: - """ - Ensure readline is imported so that it attaches to the correct stdio + """Ensure readline is imported so that it attaches to the correct stdio handles on Windows. Pdb uses readline support where available--when not running from the Python @@ -80,7 +75,7 @@ def _readline_workaround() -> None: workaround ensures that readline is imported before I/O capture is setup so that it can attach to the actual stdin/out for the console. - See https://github.com/pytest-dev/pytest/pull/1281 + See https://github.com/pytest-dev/pytest/pull/1281. """ if sys.platform.startswith("win32"): try: @@ -90,8 +85,9 @@ def _readline_workaround() -> None: def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: - """ - Python 3.6 implemented unicode console handling for Windows. This works + """Workaround for Windows Unicode console handling on Python>=3.6. + + Python 3.6 implemented Unicode console handling for Windows. This works by reading/writing to the raw console handle using ``{Read,Write}ConsoleW``. @@ -106,10 +102,11 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: also means a different handle by replicating the logic in "Py_lifecycle.c:initstdio/create_stdio". - :param stream: in practice ``sys.stdout`` or ``sys.stderr``, but given + :param stream: + In practice ``sys.stdout`` or ``sys.stderr``, but given here as parameter for unittesting purposes. - See https://github.com/pytest-dev/py/issues/103 + See https://github.com/pytest-dev/py/issues/103. """ if ( not sys.platform.startswith("win32") @@ -118,7 +115,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: ): return - # bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666) + # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666). if not hasattr(stream, "buffer"): return @@ -158,10 +155,10 @@ def pytest_load_initial_conftests(early_config: Config): capman = CaptureManager(ns.capture) pluginmanager.register(capman, "capturemanager") - # make sure that capturemanager is properly reset at final shutdown + # Make sure that capturemanager is properly reset at final shutdown. early_config.add_cleanup(capman.stop_global_capturing) - # finally trigger conftest loading but while capturing (issue93) + # Finally trigger conftest loading but while capturing (issue #93). capman.start_global_capturing() outcome = yield capman.suspend_global_capture() @@ -347,9 +344,9 @@ def writeorg(self, data): class FDCaptureBinary: - """Capture IO to/from a given os-level filedescriptor. + """Capture IO to/from a given OS-level file descriptor. - snap() produces `bytes` + snap() produces `bytes`. """ EMPTY_BUFFER = b"" @@ -415,7 +412,7 @@ def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: ) def start(self) -> None: - """ Start capturing on targetfd using memorized tmpfile. """ + """Start capturing on targetfd using memorized tmpfile.""" self._assert_state("start", ("initialized",)) os.dup2(self.tmpfile.fileno(), self.targetfd) self.syscapture.start() @@ -430,8 +427,8 @@ def snap(self): return res def done(self) -> None: - """ stop capturing, restore streams, return original capture file, - seeked to position zero. """ + """Stop capturing, restore streams, return original capture file, + seeked to position zero.""" self._assert_state("done", ("initialized", "started", "suspended", "done")) if self._state == "done": return @@ -462,15 +459,15 @@ def resume(self) -> None: self._state = "started" def writeorg(self, data): - """ write to original file descriptor. """ + """Write to original file descriptor.""" self._assert_state("writeorg", ("started", "suspended")) os.write(self.targetfd_save, data) class FDCapture(FDCaptureBinary): - """Capture IO to/from a given os-level filedescriptor. + """Capture IO to/from a given OS-level file descriptor. - snap() produces text + snap() produces text. """ # Ignore type because it doesn't match the type in the superclass (bytes). @@ -485,7 +482,7 @@ def snap(self): return res def writeorg(self, data): - """ write to original file descriptor. """ + """Write to original file descriptor.""" super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream @@ -518,7 +515,7 @@ def start_capturing(self) -> None: self.err.start() def pop_outerr_to_orig(self): - """ pop current snapshot out/err capture and flush to orig streams. """ + """Pop current snapshot out/err capture and flush to orig streams.""" out, err = self.readouterr() if out: self.out.writeorg(out) @@ -547,7 +544,7 @@ def resume_capturing(self) -> None: self._in_suspended = False def stop_capturing(self) -> None: - """ stop capturing and reset capturing streams """ + """Stop capturing and reset capturing streams.""" if self._state == "stopped": raise ValueError("was already stopped") self._state = "stopped" @@ -588,16 +585,22 @@ def _get_multicapture(method: "_CaptureMethod") -> MultiCapture: class CaptureManager: - """ - Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each - test phase (setup, call, teardown). After each of those points, the captured output is obtained and - attached to the collection/runtest report. + """The capture plugin. + + Manages that the appropriate capture method is enabled/disabled during + collection and each test phase (setup, call, teardown). After each of + those points, the captured output is obtained and attached to the + collection/runtest report. There are two levels of capture: - * global: which is enabled by default and can be suppressed by the ``-s`` option. This is always enabled/disabled - during collection and each test phase. - * fixture: when a test function or one of its fixture depend on the ``capsys`` or ``capfd`` fixtures. In this - case special handling is needed to ensure the fixtures take precedence over the global capture. + + * global: enabled by default and can be suppressed by the ``-s`` + option. This is always enabled/disabled during collection and each test + phase. + + * fixture: when a test function or one of its fixture depend on the + ``capsys`` or ``capfd`` fixtures. In this case special handling is + needed to ensure the fixtures take precedence over the global capture. """ def __init__(self, method: "_CaptureMethod") -> None: @@ -673,14 +676,13 @@ def unset_fixture(self) -> None: self._capture_fixture = None def activate_fixture(self) -> None: - """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over - the global capture. - """ + """If the current item is using ``capsys`` or ``capfd``, activate + them so they take precedence over the global capture.""" if self._capture_fixture: self._capture_fixture._start() def deactivate_fixture(self) -> None: - """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any.""" + """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any.""" if self._capture_fixture: self._capture_fixture.close() @@ -759,10 +761,8 @@ def pytest_internalerror(self) -> None: class CaptureFixture: - """ - Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary` - fixtures. - """ + """Object returned by the :py:func:`capsys`, :py:func:`capsysbinary`, + :py:func:`capfd` and :py:func:`capfdbinary` fixtures.""" def __init__(self, captureclass, request: SubRequest) -> None: self.captureclass = captureclass @@ -787,9 +787,12 @@ def close(self) -> None: self._capture = None def readouterr(self): - """Read and return the captured output so far, resetting the internal buffer. + """Read and return the captured output so far, resetting the internal + buffer. - :return: captured content as a namedtuple with ``out`` and ``err`` string attributes + :returns: + The captured content as a namedtuple with ``out`` and ``err`` + string attributes. """ captured_out, captured_err = self._captured_out, self._captured_err if self._capture is not None: @@ -801,18 +804,18 @@ def readouterr(self): return CaptureResult(captured_out, captured_err) def _suspend(self) -> None: - """Suspends this fixture's own capturing temporarily.""" + """Suspend this fixture's own capturing temporarily.""" if self._capture is not None: self._capture.suspend_capturing() def _resume(self) -> None: - """Resumes this fixture's own capturing temporarily.""" + """Resume this fixture's own capturing temporarily.""" if self._capture is not None: self._capture.resume_capturing() @contextlib.contextmanager def disabled(self) -> Generator[None, None, None]: - """Temporarily disables capture while inside the 'with' block.""" + """Temporarily disable capturing while inside the ``with`` block.""" capmanager = self.request.config.pluginmanager.getplugin("capturemanager") with capmanager.global_and_fixture_disabled(): yield diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 4ff59e1fb5e..93232f1bfbb 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -1,6 +1,4 @@ -""" -python version compatibility code -""" +"""Python version compatibility code.""" import enum import functools import inspect @@ -73,8 +71,7 @@ def _format_args(func: Callable[..., Any]) -> str: def fspath(p): """os.fspath replacement, useful to point out when we should replace it by the - real function once we drop py35. - """ + real function once we drop py35.""" return str(p) @@ -88,8 +85,7 @@ def is_generator(func: object) -> bool: def iscoroutinefunction(func: object) -> bool: - """ - Return True if func is a coroutine function (a function defined with async + """Return True if func is a coroutine function (a function defined with async def syntax, and doesn't contain yield), or a function decorated with @asyncio.coroutine. @@ -101,7 +97,8 @@ def syntax, and doesn't contain yield), or a function decorated with def is_async_function(func: object) -> bool: - """Return True if the given function seems to be an async function or async generator""" + """Return True if the given function seems to be an async function or + an async generator.""" return iscoroutinefunction(func) or ( sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func) ) @@ -119,7 +116,7 @@ def getlocation(function, curdir=None) -> str: def num_mock_patch_args(function) -> int: - """ return number of arguments used up by mock arguments (if any) """ + """Return number of arguments used up by mock arguments (if any).""" patchings = getattr(function, "patchings", None) if not patchings: return 0 @@ -144,13 +141,13 @@ def getfuncargnames( is_method: bool = False, cls: Optional[type] = None ) -> Tuple[str, ...]: - """Returns the names of a function's mandatory arguments. + """Return the names of a function's mandatory arguments. - This should return the names of all function arguments that: - * Aren't bound to an instance or type as in instance or class methods. - * Don't have default values. - * Aren't bound with functools.partial. - * Aren't replaced with mocks. + Should return the names of all function arguments that: + * Aren't bound to an instance or type as in instance or class methods. + * Don't have default values. + * Aren't bound with functools.partial. + * Aren't replaced with mocks. The is_method and cls arguments indicate that the function should be treated as a bound method even though it's not unless, only in @@ -212,8 +209,9 @@ def nullcontext(): def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]: - # Note: this code intentionally mirrors the code at the beginning of getfuncargnames, - # to get the arguments which were excluded from its result because they had default values + # Note: this code intentionally mirrors the code at the beginning of + # getfuncargnames, to get the arguments which were excluded from its result + # because they had default values. return tuple( p.name for p in signature(function).parameters.values() @@ -242,22 +240,21 @@ def _bytes_to_ascii(val: bytes) -> str: def ascii_escaped(val: Union[bytes, str]) -> str: - """If val is pure ascii, returns it as a str(). Otherwise, escapes + r"""If val is pure ASCII, return it as an str, otherwise, escape bytes objects into a sequence of escaped bytes: - b'\xc3\xb4\xc5\xd6' -> '\\xc3\\xb4\\xc5\\xd6' + b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6' and escapes unicode objects into a sequence of escaped unicode ids, e.g.: - '4\\nV\\U00043efa\\x0eMXWB\\x1e\\u3028\\u15fd\\xcd\\U0007d944' + r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944' - note: - the obvious "v.decode('unicode-escape')" will return - valid utf-8 unicode if it finds them in bytes, but we + Note: + The obvious "v.decode('unicode-escape')" will return + valid UTF-8 unicode if it finds them in bytes, but we want to return escaped bytes for any byte, even if they match - a utf-8 string. - + a UTF-8 string. """ if isinstance(val, bytes): ret = _bytes_to_ascii(val) @@ -270,18 +267,17 @@ def ascii_escaped(val: Union[bytes, str]) -> str: class _PytestWrapper: """Dummy wrapper around a function object for internal use only. - Used to correctly unwrap the underlying function object - when we are creating fixtures, because we wrap the function object ourselves with a decorator - to issue warnings when the fixture function is called directly. + Used to correctly unwrap the underlying function object when we are + creating fixtures, because we wrap the function object ourselves with a + decorator to issue warnings when the fixture function is called directly. """ obj = attr.ib() def get_real_func(obj): - """ gets the real function object of the (possibly) wrapped object by - functools.wraps or functools.partial. - """ + """Get the real function object of the (possibly) wrapped object by + functools.wraps or functools.partial.""" start_obj = obj for i in range(100): # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function @@ -307,10 +303,9 @@ def get_real_func(obj): def get_real_method(obj, holder): - """ - Attempts to obtain the real function object that might be wrapping ``obj``, while at the same time - returning a bound method to ``holder`` if the original object was a bound method. - """ + """Attempt to obtain the real function object that might be wrapping + ``obj``, while at the same time returning a bound method to ``holder`` if + the original object was a bound method.""" try: is_method = hasattr(obj, "__func__") obj = get_real_func(obj) @@ -329,12 +324,13 @@ def getimfunc(func): def safe_getattr(object: Any, name: str, default: Any) -> Any: - """ Like getattr but return default upon any Exception or any OutcomeException. + """Like getattr but return default upon any Exception or any OutcomeException. Attribute access can potentially fail for 'evil' Python objects. See issue #214. - It catches OutcomeException because of #2490 (issue #580), new outcomes are derived from BaseException - instead of Exception (for more details check #2707) + It catches OutcomeException because of #2490 (issue #580), new outcomes + are derived from BaseException instead of Exception (for more details + check #2707). """ try: return getattr(object, name, default) @@ -427,7 +423,7 @@ def __get__(self, instance, owner=None): # noqa: F811 # # With `assert_never` we can do better: # -# // throw new Error('unreachable'); +# // raise Exception('unreachable') # return assert_never(x) # # Now, if we forget to handle the new variant, the type-checker will emit a diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 188cccd1a65..e0c463d2f4c 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1,4 +1,4 @@ -""" command line options, ini-file and conftest.py processing. """ +"""Command line options, ini-file and conftest.py processing.""" import argparse import collections.abc import contextlib @@ -34,7 +34,7 @@ import _pytest._code import _pytest.deprecated -import _pytest.hookspec # the extension point definitions +import _pytest.hookspec from .exceptions import PrintHelp as PrintHelp from .exceptions import UsageError as UsageError from .findpaths import determine_setup @@ -61,9 +61,12 @@ _PluggyPlugin = object """A type to represent plugin objects. + Plugins can be any namespace, so we can't narrow it down much, but we use an alias to make the intent clear. -Ideally this type would be provided by pluggy itself.""" + +Ideally this type would be provided by pluggy itself. +""" hookimpl = HookimplMarker("pytest") @@ -71,25 +74,24 @@ class ExitCode(enum.IntEnum): - """ - .. versionadded:: 5.0 - - Encodes the valid exit codes by pytest. + """Encodes the valid exit codes by pytest. Currently users and plugins may supply other exit codes as well. + + .. versionadded:: 5.0 """ - #: tests passed + #: Tests passed. OK = 0 - #: tests failed + #: Tests failed. TESTS_FAILED = 1 - #: pytest was interrupted + #: pytest was interrupted. INTERRUPTED = 2 - #: an internal error got in the way + #: An internal error got in the way. INTERNAL_ERROR = 3 - #: pytest was misused + #: pytest was misused. USAGE_ERROR = 4 - #: pytest couldn't find tests + #: pytest couldn't find tests. NO_TESTS_COLLECTED = 5 @@ -112,7 +114,7 @@ def __str__(self) -> str: def filter_traceback_for_conftest_import_failure( entry: _pytest._code.TracebackEntry, ) -> bool: - """filters tracebacks entries which point to pytest internals or importlib. + """Filter tracebacks entries which point to pytest internals or importlib. Make a special case for importlib because we use it to import test modules and conftest files in _pytest.pathlib.import_path. @@ -124,12 +126,12 @@ def main( args: Optional[List[str]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, ) -> Union[int, ExitCode]: - """ return exit code, after performing an in-process test run. + """Perform an in-process test run. - :arg args: list of command line arguments. + :param args: List of command line arguments. + :param plugins: List of plugin objects to be auto-registered during initialization. - :arg plugins: list of plugin objects to be auto-registered during - initialization. + :returns: An exit code. """ try: try: @@ -171,7 +173,7 @@ def main( def console_main() -> int: - """pytest's CLI entry point. + """The CLI entry point of pytest. This function is not meant for programmable use; use `main()` instead. """ @@ -193,10 +195,10 @@ class cmdline: # compatibility namespace def filename_arg(path: str, optname: str) -> str: - """ Argparse type validator for filename arguments. + """Argparse type validator for filename arguments. - :path: path of filename - :optname: name of the option + :path: Path of filename. + :optname: Name of the option. """ if os.path.isdir(path): raise UsageError("{} must be a filename, given: {}".format(optname, path)) @@ -206,8 +208,8 @@ def filename_arg(path: str, optname: str) -> str: def directory_arg(path: str, optname: str) -> str: """Argparse type validator for directory arguments. - :path: path of directory - :optname: name of the option + :path: Path of directory. + :optname: Name of the option. """ if not os.path.isdir(path): raise UsageError("{} must be a directory, given: {}".format(optname, path)) @@ -278,8 +280,7 @@ def get_config( def get_plugin_manager() -> "PytestPluginManager": - """ - Obtain a new instance of the + """Obtain a new instance of the :py:class:`_pytest.config.PytestPluginManager`, with default plugins already loaded. @@ -320,13 +321,12 @@ def _prepareconfig( class PytestPluginManager(PluginManager): - """ - Overwrites :py:class:`pluggy.PluginManager ` to add pytest-specific - functionality: + """A :py:class:`pluggy.PluginManager ` with + additional pytest-specific functionality: - * loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and - ``pytest_plugins`` global variables found in plugins being loaded; - * ``conftest.py`` loading during start-up; + * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and + ``pytest_plugins`` global variables found in plugins being loaded. + * ``conftest.py`` loading during start-up. """ def __init__(self) -> None: @@ -359,27 +359,27 @@ def __init__(self) -> None: # Config._consider_importhook will set a real object if required. self.rewrite_hook = _pytest.assertion.DummyRewriteHook() - # Used to know when we are importing conftests after the pytest_configure stage + # Used to know when we are importing conftests after the pytest_configure stage. self._configured = False def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str): - # pytest hooks are always prefixed with pytest_ + # pytest hooks are always prefixed with "pytest_", # so we avoid accessing possibly non-readable attributes - # (see issue #1073) + # (see issue #1073). if not name.startswith("pytest_"): return - # ignore names which can not be hooks + # Ignore names which can not be hooks. if name == "pytest_plugins": return method = getattr(plugin, name) opts = super().parse_hookimpl_opts(plugin, name) - # consider only actual functions for hooks (#3775) + # Consider only actual functions for hooks (#3775). if not inspect.isroutine(method): return - # collect unmarked hooks as long as they have the `pytest_' prefix + # Collect unmarked hooks as long as they have the `pytest_' prefix. if opts is None and name.startswith("pytest_"): opts = {} if opts is not None: @@ -432,17 +432,18 @@ def register( return ret def getplugin(self, name: str): - # support deprecated naming because plugins (xdist e.g.) use it + # Support deprecated naming because plugins (xdist e.g.) use it. plugin = self.get_plugin(name) # type: Optional[_PluggyPlugin] return plugin def hasplugin(self, name: str) -> bool: - """Return True if the plugin with the given name is registered.""" + """Return whether a plugin with the given name is registered.""" return bool(self.get_plugin(name)) def pytest_configure(self, config: "Config") -> None: + """:meta private:""" # XXX now that the pluginmanager exposes hookimpl(tryfirst...) - # we should remove tryfirst/trylast as markers + # we should remove tryfirst/trylast as markers. config.addinivalue_line( "markers", "tryfirst: mark a hook implementation function such that the " @@ -456,15 +457,15 @@ def pytest_configure(self, config: "Config") -> None: self._configured = True # - # internal API for local conftest plugin handling + # Internal API for local conftest plugin handling. # def _set_initial_conftests(self, namespace: argparse.Namespace) -> None: - """ load initial conftest files given a preparsed "namespace". - As conftest files may add their own command line options - which have arguments ('--my-opt somepath') we might get some - false positives. All builtin and 3rd party plugins will have - been loaded, however, so common options will not confuse our logic - here. + """Load initial conftest files given a preparsed "namespace". + + As conftest files may add their own command line options which have + arguments ('--my-opt somepath') we might get some false positives. + All builtin and 3rd party plugins will have been loaded, however, so + common options will not confuse our logic here. """ current = py.path.local() self._confcutdir = ( @@ -513,7 +514,7 @@ def _getconftestmodules( # XXX these days we may rather want to use config.rootdir # and allow users to opt into looking into the rootdir parent - # directories instead of requiring to specify confcutdir + # directories instead of requiring to specify confcutdir. clist = [] for parent in directory.parts(): if self._confcutdir and self._confcutdir.relto(parent): @@ -539,8 +540,8 @@ def _rget_with_confmod( def _importconftest( self, conftestpath: py.path.local, importmode: Union[str, ImportMode], ) -> types.ModuleType: - # Use a resolved Path object as key to avoid loading the same conftest twice - # with build systems that create build directories containing + # Use a resolved Path object as key to avoid loading the same conftest + # twice with build systems that create build directories containing # symlinks to actual files. # Using Path().resolve() is better than py.path.realpath because # it resolves to the correct path/drive in case-insensitive file systems (#5792) @@ -627,7 +628,7 @@ def consider_pluginarg(self, arg: str) -> None: if name in essential_plugins: raise UsageError("plugin %s cannot be disabled" % name) - # PR #4304 : remove stepwise if cacheprovider is blocked + # PR #4304: remove stepwise if cacheprovider is blocked. if name == "cacheprovider": self.set_blocked("stepwise") self.set_blocked("pytest_stepwise") @@ -663,11 +664,12 @@ def _import_plugin_specs( self.import_plugin(import_spec) def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None: + """Import a plugin with ``modname``. + + If ``consider_entry_points`` is True, entry point names are also + considered to find a plugin. """ - Imports a plugin with ``modname``. If ``consider_entry_points`` is True, entry point - names are also considered to find a plugin. - """ - # most often modname refers to builtin modules, e.g. "pytester", + # Most often modname refers to builtin modules, e.g. "pytester", # "terminal" or "capture". Those plugins are registered under their # basename for historic purposes but must be imported with the # _pytest prefix. @@ -743,10 +745,11 @@ def __repr__(self): def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: - """ - Given an iterable of file names in a source distribution, return the "names" that should - be marked for assertion rewrite (for example the package "pytest_mock/__init__.py" should - be added as "pytest_mock" in the assertion rewrite mechanism. + """Given an iterable of file names in a source distribution, return the "names" that should + be marked for assertion rewrite. + + For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in + the assertion rewrite mechanism. This function has to deal with dist-info based distributions and egg based distributions (which are still very much in use for "editable" installs). @@ -790,11 +793,11 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: yield package_name if not seen_some: - # at this point we did not find any packages or modules suitable for assertion + # At this point we did not find any packages or modules suitable for assertion # rewriting, so we try again by stripping the first path component (to account for - # "src" based source trees for example) - # this approach lets us have the common case continue to be fast, as egg-distributions - # are rarer + # "src" based source trees for example). + # This approach lets us have the common case continue to be fast, as egg-distributions + # are rarer. new_package_files = [] for fn in package_files: parts = fn.split("/") @@ -810,8 +813,7 @@ def _args_converter(args: Iterable[str]) -> Tuple[str, ...]: class Config: - """ - Access to configuration values, pluginmanager and plugin hooks. + """Access to configuration values, pluginmanager and plugin hooks. :param PytestPluginManager pluginmanager: @@ -837,11 +839,11 @@ class InvocationParams: """ args = attr.ib(type=Tuple[str, ...], converter=_args_converter) - """tuple of command-line arguments as passed to ``pytest.main()``.""" + """Tuple of command-line arguments as passed to ``pytest.main()``.""" plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]]) - """list of extra plugins, might be `None`.""" + """List of extra plugins, might be `None`.""" dir = attr.ib(type=Path) - """directory where ``pytest.main()`` was invoked from.""" + """Directory from which ``pytest.main()`` was invoked.""" def __init__( self, @@ -857,9 +859,10 @@ def __init__( ) self.option = argparse.Namespace() - """access to command line option as attributes. + """Access to command line option as attributes. - :type: argparse.Namespace""" + :type: argparse.Namespace + """ self.invocation_params = invocation_params @@ -869,9 +872,10 @@ def __init__( processopt=self._processopt, ) self.pluginmanager = pluginmanager - """the plugin manager handles plugin registration and hook invocation. + """The plugin manager handles plugin registration and hook invocation. - :type: PytestPluginManager""" + :type: PytestPluginManager. + """ self.trace = self.pluginmanager.trace.root.get("config") self.hook = self.pluginmanager.hook @@ -895,11 +899,11 @@ def __init__( @property def invocation_dir(self) -> py.path.local: - """Backward compatibility""" + """Backward compatibility.""" return py.path.local(str(self.invocation_params.dir)) def add_cleanup(self, func: Callable[[], None]) -> None: - """ Add a function to be called when the config object gets out of + """Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" self._cleanup.append(func) @@ -970,7 +974,7 @@ def notify_exception( sys.stderr.flush() def cwd_relative_nodeid(self, nodeid: str) -> str: - # nodeid's are relative to the rootpath, compute relative to cwd + # nodeid's are relative to the rootpath, compute relative to cwd. if self.invocation_dir != self.rootdir: fullpath = self.rootdir.join(nodeid) nodeid = self.invocation_dir.bestrelpath(fullpath) @@ -978,7 +982,7 @@ def cwd_relative_nodeid(self, nodeid: str) -> str: @classmethod def fromdictargs(cls, option_dict, args) -> "Config": - """ constructor usable for subprocesses. """ + """Constructor usable for subprocesses.""" config = get_config(args) config.option.__dict__.update(option_dict) config.parse(args, addopts=False) @@ -1041,11 +1045,9 @@ def _consider_importhook(self, args: Sequence[str]) -> None: self._warn_about_missing_assertion(mode) def _mark_plugins_for_rewrite(self, hook) -> None: - """ - Given an importhook, mark for rewrite any top-level + """Given an importhook, mark for rewrite any top-level modules or packages in the distribution package for - all pytest plugins. - """ + all pytest plugins.""" self.pluginmanager.rewrite_hook = hook if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): @@ -1194,7 +1196,7 @@ def _get_unknown_ini_keys(self) -> List[str]: return [name for name in self.inicfg if name not in parser_inicfg] def parse(self, args: List[str], addopts: bool = True) -> None: - # parse given cmdline arguments into this config object. + # Parse given cmdline arguments into this config object. assert not hasattr( self, "args" ), "can only parse cmdline args at most once per Config object" @@ -1219,18 +1221,20 @@ def parse(self, args: List[str], addopts: bool = True) -> None: pass def addinivalue_line(self, name: str, line: str) -> None: - """ add a line to an ini-file option. The option must have been - declared but might not yet be set in which case the line becomes the - the first line in its value. """ + """Add a line to an ini-file option. The option must have been + declared but might not yet be set in which case the line becomes + the first line in its value.""" x = self.getini(name) assert isinstance(x, list) x.append(line) # modifies the cached list inline def getini(self, name: str): - """ return configuration value from an :ref:`ini file `. If the - specified name hasn't been registered through a prior + """Return configuration value from an :ref:`ini file `. + + If the specified name hasn't been registered through a prior :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>` - call (usually from a plugin), a ValueError is raised. """ + call (usually from a plugin), a ValueError is raised. + """ try: return self._inicache[name] except KeyError: @@ -1254,19 +1258,20 @@ def _getini(self, name: str): return [] else: value = override_value - # coerce the values based on types - # note: some coercions are only required if we are reading from .ini files, because + # Coerce the values based on types. + # + # Note: some coercions are only required if we are reading from .ini files, because # the file format doesn't contain type information, but when reading from toml we will # get either str or list of str values (see _parse_ini_config_from_pyproject_toml). - # for example: + # For example: # # ini: # a_line_list = "tests acceptance" - # in this case, we need to split the string to obtain a list of strings + # in this case, we need to split the string to obtain a list of strings. # # toml: # a_line_list = ["tests", "acceptance"] - # in this case, we already have a list ready to use + # in this case, we already have a list ready to use. # if type == "pathlist": # TODO: This assert is probably not valid in all cases. @@ -1307,9 +1312,9 @@ def _getconftest_pathlist( def _get_override_ini_value(self, name: str) -> Optional[str]: value = None - # override_ini is a list of "ini=value" options - # always use the last item if multiple values are set for same ini-name, - # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2 + # override_ini is a list of "ini=value" options. + # Always use the last item if multiple values are set for same ini-name, + # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2. for ini_config in self._override_ini: try: key, user_ini_value = ini_config.split("=", 1) @@ -1325,12 +1330,12 @@ def _get_override_ini_value(self, name: str) -> Optional[str]: return value def getoption(self, name: str, default=notset, skip: bool = False): - """ return command line option value. + """Return command line option value. - :arg name: name of the option. You may also specify + :param name: Name of the option. You may also specify the literal ``--OPT`` option instead of the "dest" option name. - :arg default: default value if no option of that name exists. - :arg skip: if True raise pytest.skip if option does not exists + :param default: Default value if no option of that name exists. + :param skip: If True, raise pytest.skip if option does not exists or has a None value. """ name = self._opt2dest.get(name, name) @@ -1349,11 +1354,11 @@ def getoption(self, name: str, default=notset, skip: bool = False): raise ValueError("no option named {!r}".format(name)) from e def getvalue(self, name: str, path=None): - """ (deprecated, use getoption()) """ + """Deprecated, use getoption() instead.""" return self.getoption(name) def getvalueorskip(self, name: str, path=None): - """ (deprecated, use getoption(skip=True)) """ + """Deprecated, use getoption(skip=True) instead.""" return self.getoption(name, skip=True) def _warn_about_missing_assertion(self, mode: str) -> None: @@ -1392,10 +1397,13 @@ def create_terminal_writer( config: Config, file: Optional[TextIO] = None ) -> TerminalWriter: """Create a TerminalWriter instance configured according to the options - in the config object. Every code which requires a TerminalWriter object - and has access to a config object should use this function. + in the config object. + + Every code which requires a TerminalWriter object and has access to a + config object should use this function. """ tw = TerminalWriter(file=file) + if config.option.color == "yes": tw.hasmarkup = True elif config.option.color == "no": @@ -1405,6 +1413,7 @@ def create_terminal_writer( tw.code_highlight = True elif config.option.code_highlight == "no": tw.code_highlight = False + return tw @@ -1415,7 +1424,7 @@ def _strtobool(val: str) -> bool: are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if 'val' is anything else. - .. note:: copied from distutils.util + .. note:: Copied from distutils.util. """ val = val.lower() if val in ("y", "yes", "t", "true", "on", "1"): diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 084ce16e59b..6c6feff4206 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -27,9 +27,9 @@ class Parser: - """ Parser for command line arguments and ini-file values. + """Parser for command line arguments and ini-file values. - :ivar extra_info: dict of generic param -> value to display in case + :ivar extra_info: Dict of generic param -> value to display in case there's an error processing the command line arguments. """ @@ -56,11 +56,11 @@ def processoption(self, option: "Argument") -> None: def getgroup( self, name: str, description: str = "", after: Optional[str] = None ) -> "OptionGroup": - """ get (or create) a named option Group. + """Get (or create) a named option Group. - :name: name of the option group. - :description: long description for --help output. - :after: name of other group, used for ordering --help output. + :name: Name of the option group. + :description: Long description for --help output. + :after: Name of another group, used for ordering --help output. The returned group object has an ``addoption`` method with the same signature as :py:func:`parser.addoption @@ -79,15 +79,14 @@ def getgroup( return group def addoption(self, *opts: str, **attrs: Any) -> None: - """ register a command line option. + """Register a command line option. - :opts: option names, can be short or long options. - :attrs: same attributes which the ``add_argument()`` function of the - `argparse library - `_ + :opts: Option names, can be short or long options. + :attrs: Same attributes which the ``add_argument()`` function of the + `argparse library `_ accepts. - After command line parsing options are available on the pytest config + After command line parsing, options are available on the pytest config object via ``config.option.NAME`` where ``NAME`` is usually set by passing a ``dest`` attribute, for example ``addoption("--long", dest="NAME", ...)``. @@ -141,9 +140,7 @@ def parse_known_args( args: Sequence[Union[str, py.path.local]], namespace: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: - """parses and returns a namespace object with known arguments at this - point. - """ + """Parse and return a namespace object with known arguments at this point.""" return self.parse_known_and_unknown_args(args, namespace=namespace)[0] def parse_known_and_unknown_args( @@ -151,9 +148,8 @@ def parse_known_and_unknown_args( args: Sequence[Union[str, py.path.local]], namespace: Optional[argparse.Namespace] = None, ) -> Tuple[argparse.Namespace, List[str]]: - """parses and returns a namespace object with known arguments, and - the remaining arguments unknown at this point. - """ + """Parse and return a namespace object with known arguments, and + the remaining arguments unknown at this point.""" optparser = self._getparser() strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] return optparser.parse_known_args(strargs, namespace=namespace) @@ -165,12 +161,12 @@ def addini( type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None, default=None, ) -> None: - """ register an ini-file option. + """Register an ini-file option. - :name: name of the ini-variable - :type: type of the variable, can be ``pathlist``, ``args``, ``linelist`` + :name: Name of the ini-variable. + :type: Type of the variable, can be ``pathlist``, ``args``, ``linelist`` or ``bool``. - :default: default value if no ini-file option exists but is queried. + :default: Default value if no ini-file option exists but is queried. The value of ini-variables can be retrieved via a call to :py:func:`config.getini(name) <_pytest.config.Config.getini>`. @@ -181,10 +177,8 @@ def addini( class ArgumentError(Exception): - """ - Raised if an Argument instance is created with invalid or - inconsistent arguments. - """ + """Raised if an Argument instance is created with invalid or + inconsistent arguments.""" def __init__(self, msg: str, option: Union["Argument", str]) -> None: self.msg = msg @@ -198,17 +192,18 @@ def __str__(self) -> str: class Argument: - """class that mimics the necessary behaviour of optparse.Option + """Class that mimics the necessary behaviour of optparse.Option. + + It's currently a least effort implementation and ignoring choices + and integer prefixes. - it's currently a least effort implementation - and ignoring choices and integer prefixes https://docs.python.org/3/library/optparse.html#optparse-standard-option-types """ _typ_map = {"int": int, "string": str, "float": float, "complex": complex} def __init__(self, *names: str, **attrs: Any) -> None: - """store parms in private vars for use in add_argument""" + """Store parms in private vars for use in add_argument.""" self._attrs = attrs self._short_opts = [] # type: List[str] self._long_opts = [] # type: List[str] @@ -224,7 +219,7 @@ def __init__(self, *names: str, **attrs: Any) -> None: except KeyError: pass else: - # this might raise a keyerror as well, don't want to catch that + # This might raise a keyerror as well, don't want to catch that. if isinstance(typ, str): if typ == "choice": warnings.warn( @@ -247,12 +242,12 @@ def __init__(self, *names: str, **attrs: Any) -> None: stacklevel=4, ) attrs["type"] = Argument._typ_map[typ] - # used in test_parseopt -> test_parse_defaultgetter + # Used in test_parseopt -> test_parse_defaultgetter. self.type = attrs["type"] else: self.type = typ try: - # attribute existence is tested in Config._processopt + # Attribute existence is tested in Config._processopt. self.default = attrs["default"] except KeyError: pass @@ -273,7 +268,7 @@ def names(self) -> List[str]: return self._short_opts + self._long_opts def attrs(self) -> Mapping[str, Any]: - # update any attributes set by processopt + # Update any attributes set by processopt. attrs = "default dest help".split() attrs.append(self.dest) for attr in attrs: @@ -289,9 +284,10 @@ def attrs(self) -> Mapping[str, Any]: return self._attrs def _set_opt_strings(self, opts: Sequence[str]) -> None: - """directly from optparse + """Directly from optparse. - might not be necessary as this is passed to argparse later on""" + Might not be necessary as this is passed to argparse later on. + """ for opt in opts: if len(opt) < 2: raise ArgumentError( @@ -340,12 +336,12 @@ def __init__( self.parser = parser def addoption(self, *optnames: str, **attrs: Any) -> None: - """ add an option to this group. + """Add an option to this group. - if a shortened version of a long option is specified it will + If a shortened version of a long option is specified, it will be suppressed in the help. addoption('--twowords', '--two-words') results in help showing '--two-words' only, but --twowords gets - accepted **and** the automatic destination is in args.twowords + accepted **and** the automatic destination is in args.twowords. """ conflict = set(optnames).intersection( name for opt in self.options for name in opt.names() @@ -386,7 +382,7 @@ def __init__( allow_abbrev=False, ) # extra_info is a dict of (param -> value) to display if there's - # an usage error to provide more contextual information to the user + # an usage error to provide more contextual information to the user. self.extra_info = extra_info if extra_info else {} def error(self, message: str) -> "NoReturn": @@ -405,7 +401,7 @@ def parse_args( # type: ignore args: Optional[Sequence[str]] = None, namespace: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: - """allow splitting of positional arguments""" + """Allow splitting of positional arguments.""" parsed, unrecognized = self.parse_known_args(args, namespace) if unrecognized: for arg in unrecognized: @@ -457,15 +453,15 @@ def _parse_optional( class DropShorterLongHelpFormatter(argparse.HelpFormatter): - """shorten help for long options that differ only in extra hyphens + """Shorten help for long options that differ only in extra hyphens. - - collapse **long** options that are the same except for extra hyphens - - shortcut if there are only two options and one of them is a short one - - cache result on action object as this is called at least 2 times + - Collapse **long** options that are the same except for extra hyphens. + - Shortcut if there are only two options and one of them is a short one. + - Cache result on the action object as this is called at least 2 times. """ def __init__(self, *args: Any, **kwargs: Any) -> None: - """Use more accurate terminal width via pylib.""" + # Use more accurate terminal width. if "width" not in kwargs: kwargs["width"] = _pytest._io.get_terminal_width() super().__init__(*args, **kwargs) diff --git a/src/_pytest/config/exceptions.py b/src/_pytest/config/exceptions.py index 19fe5cb08ed..95c412734be 100644 --- a/src/_pytest/config/exceptions.py +++ b/src/_pytest/config/exceptions.py @@ -1,9 +1,7 @@ class UsageError(Exception): - """ error in pytest usage or invocation""" + """Error in pytest usage or invocation.""" class PrintHelp(Exception): - """Raised when pytest should print it's help to skip the rest of the + """Raised when pytest should print its help to skip the rest of the argument parsing and validation.""" - - pass diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 08a71122dcd..dcd0be9ed15 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -18,10 +18,10 @@ def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig: - """Parses the given generic '.ini' file using legacy IniConfig parser, returning + """Parse the given generic '.ini' file using legacy IniConfig parser, returning the parsed object. - Raises UsageError if the file cannot be parsed. + Raise UsageError if the file cannot be parsed. """ try: return iniconfig.IniConfig(path) @@ -32,23 +32,23 @@ def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig: def load_config_dict_from_file( filepath: py.path.local, ) -> Optional[Dict[str, Union[str, List[str]]]]: - """Loads pytest configuration from the given file path, if supported. + """Load pytest configuration from the given file path, if supported. Return None if the file does not contain valid pytest configuration. """ - # configuration from ini files are obtained from the [pytest] section, if present. + # Configuration from ini files are obtained from the [pytest] section, if present. if filepath.ext == ".ini": iniconfig = _parse_ini_config(filepath) if "pytest" in iniconfig: return dict(iniconfig["pytest"].items()) else: - # "pytest.ini" files are always the source of configuration, even if empty + # "pytest.ini" files are always the source of configuration, even if empty. if filepath.basename == "pytest.ini": return {} - # '.cfg' files are considered if they contain a "[tool:pytest]" section + # '.cfg' files are considered if they contain a "[tool:pytest]" section. elif filepath.ext == ".cfg": iniconfig = _parse_ini_config(filepath) @@ -59,7 +59,7 @@ def load_config_dict_from_file( # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) - # '.toml' files are considered if they contain a [tool.pytest.ini_options] table + # '.toml' files are considered if they contain a [tool.pytest.ini_options] table. elif filepath.ext == ".toml": import toml @@ -83,10 +83,8 @@ def locate_config( ) -> Tuple[ Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]], ]: - """ - Search in the list of arguments for a valid ini-file for pytest, - and return a tuple of (rootdir, inifile, cfg-dict). - """ + """Search in the list of arguments for a valid ini-file for pytest, + and return a tuple of (rootdir, inifile, cfg-dict).""" config_names = [ "pytest.ini", "pyproject.toml", diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 3677d3bf915..5dda4b8d71c 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -1,4 +1,4 @@ -""" interactive debugging with PDB, the Python Debugger. """ +"""Interactive debugging with PDB, the Python Debugger.""" import argparse import functools import sys @@ -87,7 +87,7 @@ def fin() -> None: class pytestPDB: - """ Pseudo PDB that defers to the real pdb. """ + """Pseudo PDB that defers to the real pdb.""" _pluginmanager = None # type: PytestPluginManager _config = None # type: Config @@ -226,7 +226,7 @@ def get_stack(self, f, t): @classmethod def _init_pdb(cls, method, *args, **kwargs): - """ Initialize PDB debugging, dropping any IO capturing. """ + """Initialize PDB debugging, dropping any IO capturing.""" import _pytest.config if cls._pluginmanager is not None: @@ -298,16 +298,16 @@ def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]: def wrap_pytest_function_for_tracing(pyfuncitem): - """Changes the python function object of the given Function item by a wrapper which actually - enters pdb before calling the python function itself, effectively leaving the user - in the pdb prompt in the first statement of the function. - """ + """Change the Python function object of the given Function item by a + wrapper which actually enters pdb before calling the python function + itself, effectively leaving the user in the pdb prompt in the first + statement of the function.""" _pdb = pytestPDB._init_pdb("runcall") testfunction = pyfuncitem.obj # we can't just return `partial(pdb.runcall, testfunction)` because (on # python < 3.7.4) runcall's first param is `func`, which means we'd get - # an exception if one of the kwargs to testfunction was called `func` + # an exception if one of the kwargs to testfunction was called `func`. @functools.wraps(testfunction) def wrapper(*args, **kwargs): func = functools.partial(testfunction, *args, **kwargs) @@ -318,7 +318,7 @@ def wrapper(*args, **kwargs): def maybe_wrap_pytest_function_for_tracing(pyfuncitem): """Wrap the given pytestfunct item for tracing support if --trace was given in - the command line""" + the command line.""" if pyfuncitem.config.getvalue("trace"): wrap_pytest_function_for_tracing(pyfuncitem) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 868318a2bc6..bd2574ba769 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -1,6 +1,5 @@ -""" -This module contains deprecation messages and bits of code used elsewhere in the codebase -that is planned to be removed in the next pytest release. +"""Deprecation messages and bits of code used elsewhere in the codebase that +is planned to be removed in the next pytest release. Keeping it in a central location makes it easy to track what is deprecated and should be removed when the time comes. diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index ebf0d584cc3..440bc649c1e 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -1,4 +1,4 @@ -""" discover and run doctests in modules and test files.""" +"""Discover and run doctests in modules and test files.""" import bdb import inspect import platform @@ -171,9 +171,10 @@ def _init_runner_class() -> "Type[doctest.DocTestRunner]": import doctest class PytestDoctestRunner(doctest.DebugRunner): - """ - Runner to collect failures. Note that the out variable in this case is - a list instead of a stdout-like object + """Runner to collect failures. + + Note that the out variable in this case is a list instead of a + stdout-like object. """ def __init__( @@ -261,9 +262,7 @@ def from_parent( # type: ignore dtest: "doctest.DocTest" ): # incompatible signature due to to imposed limits on sublcass - """ - the public named constructor - """ + """The public named constructor.""" return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) def setup(self) -> None: @@ -289,9 +288,7 @@ def runtest(self) -> None: raise MultipleDoctestFailures(failures) def _disable_output_capturing_for_darwin(self) -> None: - """ - Disable output capturing. Otherwise, stdout is lost to doctest (#985) - """ + """Disable output capturing. Otherwise, stdout is lost to doctest (#985).""" if platform.system() != "Darwin": return capman = self.config.pluginmanager.getplugin("capturemanager") @@ -403,7 +400,7 @@ def _get_continue_on_failure(config): continue_on_failure = config.getvalue("doctest_continue_on_failure") if continue_on_failure: # We need to turn off this if we use pdb since we should stop at - # the first failure + # the first failure. if config.getvalue("usepdb"): continue_on_failure = False return continue_on_failure @@ -415,8 +412,8 @@ class DoctestTextfile(pytest.Module): def collect(self) -> Iterable[DoctestItem]: import doctest - # inspired by doctest.testfile; ideally we would use it directly, - # but it doesn't support passing a custom checker + # Inspired by doctest.testfile; ideally we would use it directly, + # but it doesn't support passing a custom checker. encoding = self.config.getini("doctest_encoding") text = self.fspath.read_text(encoding) filename = str(self.fspath) @@ -441,9 +438,8 @@ def collect(self) -> Iterable[DoctestItem]: def _check_all_skipped(test: "doctest.DocTest") -> None: - """raises pytest.skip() if all examples in the given DocTest have the SKIP - option set. - """ + """Raise pytest.skip() if all examples in the given DocTest have the SKIP + option set.""" import doctest all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples) @@ -452,9 +448,8 @@ def _check_all_skipped(test: "doctest.DocTest") -> None: def _is_mocked(obj: object) -> bool: - """ - returns if a object is possibly a mock object by checking the existence of a highly improbable attribute - """ + """Return if an object is possibly a mock object by checking the + existence of a highly improbable attribute.""" return ( safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None) is not None @@ -463,10 +458,8 @@ def _is_mocked(obj: object) -> bool: @contextmanager def _patch_unwrap_mock_aware() -> Generator[None, None, None]: - """ - contextmanager which replaces ``inspect.unwrap`` with a version - that's aware of mock objects and doesn't recurse on them - """ + """Context manager which replaces ``inspect.unwrap`` with a version + that's aware of mock objects and doesn't recurse into them.""" real_unwrap = inspect.unwrap def _mock_aware_unwrap( @@ -498,16 +491,15 @@ def collect(self) -> Iterable[DoctestItem]: import doctest class MockAwareDocTestFinder(doctest.DocTestFinder): - """ - a hackish doctest finder that overrides stdlib internals to fix a stdlib bug + """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug. https://github.com/pytest-dev/pytest/issues/3456 https://bugs.python.org/issue25532 """ def _find_lineno(self, obj, source_lines): - """ - Doctest code does not take into account `@property`, this is a hackish way to fix it. + """Doctest code does not take into account `@property`, this + is a hackish way to fix it. https://bugs.python.org/issue17446 """ @@ -542,7 +534,7 @@ def _find( pytest.skip("unable to import module %r" % self.fspath) else: raise - # uses internal doctest module parsing mechanism + # Uses internal doctest module parsing mechanism. finder = MockAwareDocTestFinder() optionflags = get_optionflags(self) runner = _get_runner( @@ -560,9 +552,7 @@ def _find( def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest: - """ - Used by DoctestTextfile and DoctestItem to setup fixture information. - """ + """Used by DoctestTextfile and DoctestItem to setup fixture information.""" def func() -> None: pass @@ -582,11 +572,9 @@ def _init_checker_class() -> "Type[doctest.OutputChecker]": import re class LiteralsOutputChecker(doctest.OutputChecker): - """ - Based on doctest_nose_plugin.py from the nltk project - (https://github.com/nltk/nltk) and on the "numtest" doctest extension - by Sebastien Boisgerault (https://github.com/boisgera/numtest). - """ + # Based on doctest_nose_plugin.py from the nltk project + # (https://github.com/nltk/nltk) and on the "numtest" doctest extension + # by Sebastien Boisgerault (https://github.com/boisgera/numtest). _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE) @@ -671,8 +659,7 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: def _get_checker() -> "doctest.OutputChecker": - """ - Returns a doctest.OutputChecker subclass that supports some + """Return a doctest.OutputChecker subclass that supports some additional options: * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b'' @@ -692,36 +679,31 @@ def _get_checker() -> "doctest.OutputChecker": def _get_allow_unicode_flag() -> int: - """ - Registers and returns the ALLOW_UNICODE flag. - """ + """Register and return the ALLOW_UNICODE flag.""" import doctest return doctest.register_optionflag("ALLOW_UNICODE") def _get_allow_bytes_flag() -> int: - """ - Registers and returns the ALLOW_BYTES flag. - """ + """Register and return the ALLOW_BYTES flag.""" import doctest return doctest.register_optionflag("ALLOW_BYTES") def _get_number_flag() -> int: - """ - Registers and returns the NUMBER flag. - """ + """Register and return the NUMBER flag.""" import doctest return doctest.register_optionflag("NUMBER") def _get_report_choice(key: str) -> int: - """ - This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid - importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests. + """Return the actual `doctest` module flag value. + + We want to do it as late as possible to avoid importing `doctest` and all + its dependencies when parsing options, as it adds overhead and breaks tests. """ import doctest @@ -736,7 +718,6 @@ def _get_report_choice(key: str) -> int: @pytest.fixture(scope="session") def doctest_namespace() -> Dict[str, Any]: - """ - Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. - """ + """Fixture that returns a :py:class:`dict` that will be injected into the + namespace of doctests.""" return dict() diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 0d969840b3d..e4a952966af 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -100,8 +100,7 @@ def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: @pytest.hookimpl(tryfirst=True) def pytest_enter_pdb(self) -> None: - """Cancel any traceback dumping due to timeout before entering pdb. - """ + """Cancel any traceback dumping due to timeout before entering pdb.""" import faulthandler faulthandler.cancel_dump_traceback_later() @@ -109,8 +108,7 @@ def pytest_enter_pdb(self) -> None: @pytest.hookimpl(tryfirst=True) def pytest_exception_interact(self) -> None: """Cancel any traceback dumping due to an interactive exception being - raised. - """ + raised.""" import faulthandler faulthandler.cancel_dump_traceback_later() diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d9f91874540..5dbaf9e0696 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -166,15 +166,16 @@ def get_scope_node(node, scope): def add_funcarg_pseudo_fixture_def( collector, metafunc: "Metafunc", fixturemanager: "FixtureManager" ) -> None: - # this function will transform all collected calls to a functions + # This function will transform all collected calls to functions # if they use direct funcargs (i.e. direct parametrization) # because we want later test execution to be able to rely on # an existing FixtureDef structure for all arguments. # XXX we can probably avoid this algorithm if we modify CallSpec2 # to directly care for creating the fixturedefs within its methods. if not metafunc._calls[0].funcargs: - return # this function call does not have direct parametrization - # collect funcargs of all callspecs into a list of values + # This function call does not have direct parametrization. + return + # Collect funcargs of all callspecs into a list of values. arg2params = {} # type: Dict[str, List[object]] arg2scope = {} # type: Dict[str, _Scope] for callspec in metafunc._calls: @@ -189,11 +190,11 @@ def add_funcarg_pseudo_fixture_def( arg2scope[argname] = scopes[scopenum] callspec.funcargs.clear() - # register artificial FixtureDef's so that later at test execution + # Register artificial FixtureDef's so that later at test execution # time we can rely on a proper FixtureDef to exist for fixture setup. arg2fixturedefs = metafunc._arg2fixturedefs for argname, valuelist in arg2params.items(): - # if we have a scope that is higher than function we need + # If we have a scope that is higher than function, we need # to make sure we only ever create an according fixturedef on # a per-scope basis. We thus store and cache the fixturedef on the # node related to the scope. @@ -203,7 +204,7 @@ def add_funcarg_pseudo_fixture_def( node = get_scope_node(collector, scope) if node is None: assert scope == "class" and isinstance(collector, _pytest.python.Module) - # use module-level collector for class-scope (for now) + # Use module-level collector for class-scope (for now). node = collector if node and argname in node._name2pseudofixturedef: arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]] @@ -224,7 +225,7 @@ def add_funcarg_pseudo_fixture_def( def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: - """ return fixturemarker or None if it doesn't exist or raised + """Return fixturemarker or None if it doesn't exist or raised exceptions.""" try: fixturemarker = getattr( @@ -242,7 +243,7 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator[_Key]: - """ return list of keys for all parametrized arguments which match + """Return list of keys for all parametrized arguments which match the specified scope. """ assert scopenum < scopenum_function # function try: @@ -269,10 +270,10 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator yield key -# algorithm for sorting on a per-parametrized resource setup basis -# it is called for scopenum==0 (session) first and performs sorting +# Algorithm for sorting on a per-parametrized resource setup basis. +# It is called for scopenum==0 (session) first and performs sorting # down to the lower scopes such as to minimize number of "high scope" -# setups and teardowns +# setups and teardowns. def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]": @@ -339,7 +340,8 @@ def reorder_items_atscope( no_argkey_group[item] = None else: slicing_argkey, _ = argkeys.popitem() - # we don't have to remove relevant items from later in the deque because they'll just be ignored + # We don't have to remove relevant items from later in the + # deque because they'll just be ignored. matching_items = [ i for i in scoped_items_by_argkey[slicing_argkey] if i in items ] @@ -358,7 +360,7 @@ def reorder_items_atscope( def fillfixtures(function: "Function") -> None: - """ fill missing funcargs for a test function. """ + """Fill missing funcargs for a test function.""" # Uncomment this after 6.0 release (#7361) # warnings.warn(FILLFUNCARGS, stacklevel=2) try: @@ -373,7 +375,7 @@ def fillfixtures(function: "Function") -> None: function._fixtureinfo = fi request = function._request = FixtureRequest(function) request._fillfixtures() - # prune out funcargs for jstests + # Prune out funcargs for jstests. newfuncargs = {} for name in fi.argnames: newfuncargs[name] = function.funcargs[name] @@ -388,9 +390,9 @@ def get_direct_param_fixture_func(request): @attr.s(slots=True) class FuncFixtureInfo: - # original function argument names + # Original function argument names. argnames = attr.ib(type=Tuple[str, ...]) - # argnames that function immediately requires. These include argnames + + # Argnames that function immediately requires. These include argnames + # fixture names specified via usefixtures and via autouse=True in fixture # definitions. initialnames = attr.ib(type=Tuple[str, ...]) @@ -398,7 +400,7 @@ class FuncFixtureInfo: name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef"]]) def prune_dependency_tree(self) -> None: - """Recompute names_closure from initialnames and name2fixturedefs + """Recompute names_closure from initialnames and name2fixturedefs. Can only reduce names_closure, which means that the new closure will always be a subset of the old one. The order is preserved. @@ -412,7 +414,7 @@ def prune_dependency_tree(self) -> None: working_set = set(self.initialnames) while working_set: argname = working_set.pop() - # argname may be smth not included in the original names_closure, + # Argname may be smth not included in the original names_closure, # in which case we ignore it. This currently happens with pseudo # FixtureDefs which wrap 'get_direct_param_fixture_func(request)'. # So they introduce the new dependency 'request' which might have @@ -426,18 +428,18 @@ def prune_dependency_tree(self) -> None: class FixtureRequest: - """ A request for a fixture from a test or fixture function. + """A request for a fixture from a test or fixture function. - A request object gives access to the requesting test context - and has an optional ``param`` attribute in case - the fixture is parametrized indirectly. + A request object gives access to the requesting test context and has + an optional ``param`` attribute in case the fixture is parametrized + indirectly. """ def __init__(self, pyfuncitem) -> None: self._pyfuncitem = pyfuncitem - #: fixture for which this request is being performed + #: Fixture for which this request is being performed. self.fixturename = None # type: Optional[str] - #: Scope string, one of "function", "class", "module", "session" + #: Scope string, one of "function", "class", "module", "session". self.scope = "function" # type: _Scope self._fixture_defs = {} # type: Dict[str, FixtureDef] fixtureinfo = pyfuncitem._fixtureinfo # type: FuncFixtureInfo @@ -449,35 +451,35 @@ def __init__(self, pyfuncitem) -> None: @property def fixturenames(self) -> List[str]: - """names of all active fixtures in this request""" + """Names of all active fixtures in this request.""" result = list(self._pyfuncitem._fixtureinfo.names_closure) result.extend(set(self._fixture_defs).difference(result)) return result @property def funcargnames(self) -> List[str]: - """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + """Alias attribute for ``fixturenames`` for pre-2.3 compatibility.""" warnings.warn(FUNCARGNAMES, stacklevel=2) return self.fixturenames @property def node(self): - """ underlying collection node (depends on current request scope)""" + """Underlying collection node (depends on current request scope).""" return self._getscopeitem(self.scope) def _getnextfixturedef(self, argname: str) -> "FixtureDef": fixturedefs = self._arg2fixturedefs.get(argname, None) if fixturedefs is None: - # we arrive here because of a dynamic call to + # We arrive here because of a dynamic call to # getfixturevalue(argname) usage which was naturally - # not known at parsing/collection time + # not known at parsing/collection time. assert self._pyfuncitem.parent is not None parentid = self._pyfuncitem.parent.nodeid fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) # TODO: Fix this type ignore. Either add assert or adjust types. # Can this be None here? self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment] - # fixturedefs list is immutable so we maintain a decreasing index + # fixturedefs list is immutable so we maintain a decreasing index. index = self._arg2index.get(argname, 0) - 1 if fixturedefs is None or (-index > len(fixturedefs)): raise FixtureLookupError(argname, self) @@ -486,25 +488,25 @@ def _getnextfixturedef(self, argname: str) -> "FixtureDef": @property def config(self) -> Config: - """ the pytest config object associated with this request. """ + """The pytest config object associated with this request.""" return self._pyfuncitem.config # type: ignore[no-any-return] # noqa: F723 @scopeproperty() def function(self): - """ test function object if the request has a per-function scope. """ + """Test function object if the request has a per-function scope.""" return self._pyfuncitem.obj @scopeproperty("class") def cls(self): - """ class (can be None) where the test function was collected. """ + """Class (can be None) where the test function was collected.""" clscol = self._pyfuncitem.getparent(_pytest.python.Class) if clscol: return clscol.obj @property def instance(self): - """ instance (can be None) on which test function was collected. """ - # unittest support hack, see _pytest.unittest.TestCaseFunction + """Instance (can be None) on which test function was collected.""" + # unittest support hack, see _pytest.unittest.TestCaseFunction. try: return self._pyfuncitem._testcase except AttributeError: @@ -513,30 +515,29 @@ def instance(self): @scopeproperty() def module(self): - """ python module object where the test function was collected. """ + """Python module object where the test function was collected.""" return self._pyfuncitem.getparent(_pytest.python.Module).obj @scopeproperty() def fspath(self) -> py.path.local: - """ the file system path of the test module which collected this test. """ + """The file system path of the test module which collected this test.""" # TODO: Remove ignore once _pyfuncitem is properly typed. return self._pyfuncitem.fspath # type: ignore @property def keywords(self): - """ keywords/markers dictionary for the underlying node. """ + """Keywords/markers dictionary for the underlying node.""" return self.node.keywords @property def session(self): - """ pytest session object. """ + """Pytest session object.""" return self._pyfuncitem.session def addfinalizer(self, finalizer: Callable[[], object]) -> None: - """ add finalizer/teardown function to be called after the - last test within the requesting test context finished - execution. """ - # XXX usually this method is shadowed by fixturedef specific ones + """Add finalizer/teardown function to be called after the last test + within the requesting test context finished execution.""" + # XXX usually this method is shadowed by fixturedef specific ones. self._addfinalizer(finalizer, scope=self.scope) def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None: @@ -546,17 +547,19 @@ def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None: ) def applymarker(self, marker) -> None: - """ Apply a marker to a single test function invocation. + """Apply a marker to a single test function invocation. + This method is useful if you don't want to have a keyword/marker on all function invocations. - :arg marker: a :py:class:`_pytest.mark.MarkDecorator` object - created by a call to ``pytest.mark.NAME(...)``. + :param marker: + A :py:class:`_pytest.mark.MarkDecorator` object created by a call + to ``pytest.mark.NAME(...)``. """ self.node.add_marker(marker) def raiseerror(self, msg: Optional[str]) -> "NoReturn": - """ raise a FixtureLookupError with the given message. """ + """Raise a FixtureLookupError with the given message.""" raise self._fixturemanager.FixtureLookupError(None, self, msg) def _fillfixtures(self) -> None: @@ -567,14 +570,14 @@ def _fillfixtures(self) -> None: item.funcargs[argname] = self.getfixturevalue(argname) def getfixturevalue(self, argname: str) -> Any: - """ Dynamically run a named fixture function. + """Dynamically run a named fixture function. Declaring fixtures via function argument is recommended where possible. But if you can only decide whether to use another fixture at test setup time, you may use this function to retrieve it inside a fixture or test function body. - :raise pytest.FixtureLookupError: + :raises pytest.FixtureLookupError: If the given fixture could not be found. """ fixturedef = self._get_active_fixturedef(argname) @@ -595,8 +598,8 @@ def _get_active_fixturedef( scope = "function" # type: _Scope return PseudoFixtureDef(cached_result, scope) raise - # remove indent to prevent the python3 exception - # from leaking into the call + # Remove indent to prevent the python3 exception + # from leaking into the call. self._compute_fixture_value(fixturedef) self._fixture_defs[argname] = fixturedef return fixturedef @@ -614,10 +617,12 @@ def _get_fixturestack(self) -> List["FixtureDef"]: current = current._parent_request def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: - """ - Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will - force the FixtureDef object to throw away any previous results and compute a new fixture value, which - will be stored into the FixtureDef object itself. + """Create a SubRequest based on "self" and call the execute method + of the given FixtureDef object. + + This will force the FixtureDef object to throw away any previous + results and compute a new fixture value, which will be stored into + the FixtureDef object itself. """ # prepare a subrequest object before calling fixture function # (latter managed by fixturedef) @@ -667,18 +672,18 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: fail(msg, pytrace=False) else: param_index = funcitem.callspec.indices[argname] - # if a parametrize invocation set a scope it will override - # the static scope defined with the fixture function + # If a parametrize invocation set a scope it will override + # the static scope defined with the fixture function. paramscopenum = funcitem.callspec._arg2scopenum.get(argname) if paramscopenum is not None: scope = scopes[paramscopenum] subrequest = SubRequest(self, scope, param, param_index, fixturedef) - # check if a higher-level scoped fixture accesses a lower level one + # Check if a higher-level scoped fixture accesses a lower level one. subrequest._check_scope(argname, self.scope, scope) try: - # call the fixture function + # Call the fixture function. fixturedef.execute(request=subrequest) finally: self._schedule_finalizers(fixturedef, subrequest) @@ -686,7 +691,7 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: def _schedule_finalizers( self, fixturedef: "FixtureDef", subrequest: "SubRequest" ) -> None: - # if fixture function failed it might have registered finalizers + # If fixture function failed it might have registered finalizers. self.session._setupstate.addfinalizer( functools.partial(fixturedef.finish, request=subrequest), subrequest.node ) @@ -695,7 +700,7 @@ def _check_scope(self, argname, invoking_scope: "_Scope", requested_scope) -> No if argname == "request": return if scopemismatch(invoking_scope, requested_scope): - # try to report something helpful + # Try to report something helpful. lines = self._factorytraceback() fail( "ScopeMismatch: You tried to access the %r scoped " @@ -717,7 +722,7 @@ def _factorytraceback(self) -> List[str]: def _getscopeitem(self, scope): if scope == "function": - # this might also be a non-function Item despite its attribute name + # This might also be a non-function Item despite its attribute name. return self._pyfuncitem if scope == "package": # FIXME: _fixturedef is not defined on FixtureRequest (this class), @@ -726,7 +731,7 @@ def _getscopeitem(self, scope): else: node = get_scope_node(self._pyfuncitem, scope) if node is None and scope == "class": - # fallback to function item itself + # Fallback to function item itself. node = self._pyfuncitem assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format( scope, self._pyfuncitem @@ -738,8 +743,7 @@ def __repr__(self) -> str: class SubRequest(FixtureRequest): - """ a sub request for handling getting a fixture from a - test function/fixture. """ + """A sub request for handling getting a fixture from a test function/fixture.""" def __init__( self, @@ -750,7 +754,7 @@ def __init__( fixturedef: "FixtureDef", ) -> None: self._parent_request = request - self.fixturename = fixturedef.argname # type: str + self.fixturename = fixturedef.argname if param is not NOTSET: self.param = param self.param_index = param_index @@ -771,9 +775,9 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None: def _schedule_finalizers( self, fixturedef: "FixtureDef", subrequest: "SubRequest" ) -> None: - # if the executing fixturedef was not explicitly requested in the argument list (via + # If the executing fixturedef was not explicitly requested in the argument list (via # getfixturevalue inside the fixture call) then ensure this fixture def will be finished - # first + # first. if fixturedef.argname not in self.fixturenames: fixturedef.addfinalizer( functools.partial(self._fixturedef.finish, request=self) @@ -791,8 +795,7 @@ def scopemismatch(currentscope: "_Scope", newscope: "_Scope") -> bool: def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int: """Look up the index of ``scope`` and raise a descriptive value error - if not defined. - """ + if not defined.""" strscopes = scopes # type: Sequence[str] try: return strscopes.index(scope) @@ -806,7 +809,7 @@ def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int: class FixtureLookupError(LookupError): - """ could not return a requested Fixture (missing or invalid). """ + """Could not return a requested fixture (missing or invalid).""" def __init__( self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = None @@ -823,8 +826,8 @@ def formatrepr(self) -> "FixtureLookupErrorRepr": stack.extend(map(lambda x: x.func, self.fixturestack)) msg = self.msg if msg is not None: - # the last fixture raise an error, let's present - # it at the requesting side + # The last fixture raise an error, let's present + # it at the requesting side. stack = stack[:-1] for function in stack: fspath, lineno = getfslineno(function) @@ -925,8 +928,9 @@ def call_fixture_func( def _teardown_yield_fixture(fixturefunc, it) -> None: - """Executes the teardown of a fixture function by advancing the iterator after the - yield and ensure the iteration ends (if not it means there is more than one yield in the function)""" + """Execute the teardown of a fixture function by advancing the iterator + after the yield and ensure the iteration ends (if not it means there is + more than one yield in the function).""" try: next(it) except StopIteration: @@ -961,7 +965,7 @@ def _eval_scope_callable( class FixtureDef(Generic[_FixtureValue]): - """ A container for a factory definition. """ + """A container for a factory definition.""" def __init__( self, @@ -1023,16 +1027,15 @@ def finish(self, request: SubRequest) -> None: finally: hook = self._fixturemanager.session.gethookproxy(request.node.fspath) hook.pytest_fixture_post_finalizer(fixturedef=self, request=request) - # even if finalization fails, we invalidate - # the cached fixture value and remove - # all finalizers because they may be bound methods which will - # keep instances alive + # Even if finalization fails, we invalidate the cached fixture + # value and remove all finalizers because they may be bound methods + # which will keep instances alive. self.cached_result = None self._finalizers = [] def execute(self, request: SubRequest) -> _FixtureValue: - # get required arguments and register our own finish() - # with their finalization + # Get required arguments and register our own finish() + # with their finalization. for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) if argname != "request": @@ -1043,7 +1046,7 @@ def execute(self, request: SubRequest) -> _FixtureValue: my_cache_key = self.cache_key(request) if self.cached_result is not None: # note: comparison with `==` can fail (or be expensive) for e.g. - # numpy arrays (#6497) + # numpy arrays (#6497). cache_key = self.cached_result[1] if my_cache_key is cache_key: if self.cached_result[2] is not None: @@ -1052,8 +1055,8 @@ def execute(self, request: SubRequest) -> _FixtureValue: else: result = self.cached_result[0] return result - # we have a previous but differently parametrized fixture instance - # so we need to tear it down before creating a new one + # We have a previous but differently parametrized fixture instance + # so we need to tear it down before creating a new one. self.finish(request) assert self.cached_result is None @@ -1073,21 +1076,20 @@ def __repr__(self) -> str: def resolve_fixture_function( fixturedef: FixtureDef[_FixtureValue], request: FixtureRequest ) -> "_FixtureFunc[_FixtureValue]": - """Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific - instances and bound methods. - """ + """Get the actual callable that can be called to obtain the fixture + value, dealing with unittest-specific instances and bound methods.""" fixturefunc = fixturedef.func if fixturedef.unittest: if request.instance is not None: - # bind the unbound method to the TestCase instance + # Bind the unbound method to the TestCase instance. fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr] else: - # the fixture function needs to be bound to the actual + # The fixture function needs to be bound to the actual # request.instance so that code working with "fixturedef" behaves # as expected. if request.instance is not None: - # handle the case where fixture is defined not in a test class, but some other class - # (for example a plugin class with a fixture), see #2270 + # Handle the case where fixture is defined not in a test class, but some other class + # (for example a plugin class with a fixture), see #2270. if hasattr(fixturefunc, "__self__") and not isinstance( request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr] ): @@ -1101,7 +1103,7 @@ def resolve_fixture_function( def pytest_fixture_setup( fixturedef: FixtureDef[_FixtureValue], request: SubRequest ) -> _FixtureValue: - """ Execution of fixture setup. """ + """Execution of fixture setup.""" kwargs = {} for argname in fixturedef.argnames: fixdef = request._get_active_fixturedef(argname) @@ -1151,8 +1153,7 @@ def _params_converter( def wrap_function_to_error_out_if_called_directly(function, fixture_marker): """Wrap the given fixture function so we can raise an error about it being called directly, - instead of used as an argument in a test function. - """ + instead of used as an argument in a test function.""" message = ( 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' "but are created automatically when test functions request them as parameters.\n" @@ -1164,8 +1165,8 @@ def wrap_function_to_error_out_if_called_directly(function, fixture_marker): def result(*args, **kwargs): fail(message, pytrace=False) - # keep reference to the original function in our own custom attribute so we don't unwrap - # further than this point and lose useful wrappings like @mock.patch (#3774) + # Keep reference to the original function in our own custom attribute so we don't unwrap + # further than this point and lose useful wrappings like @mock.patch (#3774). result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined] return result @@ -1268,47 +1269,49 @@ def fixture( # noqa: F811 fixture function. The name of the fixture function can later be referenced to cause its - invocation ahead of running tests: test - modules or classes can use the ``pytest.mark.usefixtures(fixturename)`` - marker. - - Test functions can directly use fixture names as input - arguments in which case the fixture instance returned from the fixture - function will be injected. - - Fixtures can provide their values to test functions using ``return`` or ``yield`` - statements. When using ``yield`` the code block after the ``yield`` statement is executed - as teardown code regardless of the test outcome, and must yield exactly once. - - :arg scope: the scope for which this fixture is shared, one of - ``"function"`` (default), ``"class"``, ``"module"``, - ``"package"`` or ``"session"``. - - This parameter may also be a callable which receives ``(fixture_name, config)`` - as parameters, and must return a ``str`` with one of the values mentioned above. - - See :ref:`dynamic scope` in the docs for more information. - - :arg params: an optional list of parameters which will cause multiple - invocations of the fixture function and all of the tests - using it. - The current parameter is available in ``request.param``. - - :arg autouse: if True, the fixture func is activated for all tests that - can see it. If False (the default) then an explicit - reference is needed to activate the fixture. - - :arg ids: list of string ids each corresponding to the params - so that they are part of the test id. If no ids are provided - they will be generated automatically from the params. - - :arg name: the name of the fixture. This defaults to the name of the - decorated function. If a fixture is used in the same module in - which it is defined, the function name of the fixture will be - shadowed by the function arg that requests the fixture; one way - to resolve this is to name the decorated function - ``fixture_`` and then use - ``@pytest.fixture(name='')``. + invocation ahead of running tests: test modules or classes can use the + ``pytest.mark.usefixtures(fixturename)`` marker. + + Test functions can directly use fixture names as input arguments in which + case the fixture instance returned from the fixture function will be + injected. + + Fixtures can provide their values to test functions using ``return`` or + ``yield`` statements. When using ``yield`` the code block after the + ``yield`` statement is executed as teardown code regardless of the test + outcome, and must yield exactly once. + + :param scope: + The scope for which this fixture is shared; one of ``"function"`` + (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``. + + This parameter may also be a callable which receives ``(fixture_name, config)`` + as parameters, and must return a ``str`` with one of the values mentioned above. + + See :ref:`dynamic scope` in the docs for more information. + + :param params: + An optional list of parameters which will cause multiple invocations + of the fixture function and all of the tests using it. The current + parameter is available in ``request.param``. + + :param autouse: + If True, the fixture func is activated for all tests that can see it. + If False (the default), an explicit reference is needed to activate + the fixture. + + :param ids: + List of string ids each corresponding to the params so that they are + part of the test id. If no ids are provided they will be generated + automatically from the params. + + :param name: + The name of the fixture. This defaults to the name of the decorated + function. If a fixture is used in the same module in which it is + defined, the function name of the fixture will be shadowed by the + function arg that requests the fixture; one way to resolve this is to + name the decorated function ``fixture_`` and then use + ``@pytest.fixture(name='')``. """ # Positional arguments backward compatibility. # If a kwarg is equal to its default, assume it was not explicitly @@ -1377,7 +1380,7 @@ def yield_fixture( ids=None, name=None ): - """ (return a) decorator to mark a yield-fixture factory function. + """(Return a) decorator to mark a yield-fixture factory function. .. deprecated:: 3.0 Use :py:func:`pytest.fixture` directly instead. @@ -1417,8 +1420,7 @@ def pytest_addoption(parser: Parser) -> None: class FixtureManager: - """ - pytest fixtures definitions and information is stored and managed + """pytest fixture definitions and information is stored and managed from this class. During collection fm.parsefactories() is called multiple times to parse @@ -1431,7 +1433,7 @@ class FixtureManager: which themselves offer a fixturenames attribute. The FuncFixtureInfo object holds information about fixtures and FixtureDefs - relevant for a particular function. An initial list of fixtures is + relevant for a particular function. An initial list of fixtures is assembled like this: - ini-defined usefixtures @@ -1441,7 +1443,7 @@ class FixtureManager: Subsequently the funcfixtureinfo.fixturenames attribute is computed as the closure of the fixtures needed to setup the initial fixtures, - i. e. fixtures needed by fixture functions themselves are appended + i.e. fixtures needed by fixture functions themselves are appended to the fixturenames list. Upon the test-setup phases all fixturenames are instantiated, retrieved @@ -1462,13 +1464,13 @@ def __init__(self, session: "Session") -> None: session.config.pluginmanager.register(self, "funcmanage") def _get_direct_parametrize_args(self, node: "nodes.Node") -> List[str]: - """This function returns all the direct parametrization - arguments of a node, so we don't mistake them for fixtures + """Return all direct parametrization arguments of a node, so we don't + mistake them for fixtures. - Check https://github.com/pytest-dev/pytest/issues/5036 + Check https://github.com/pytest-dev/pytest/issues/5036. - This things are done later as well when dealing with parametrization - so this could be improved + These things are done later as well when dealing with parametrization + so this could be improved. """ parametrize_argnames = [] # type: List[str] for marker in node.iter_markers(name="parametrize"): @@ -1507,9 +1509,9 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: else: from _pytest import nodes - # construct the base nodeid which is later used to check + # Construct the base nodeid which is later used to check # what fixtures are visible for particular tests (as denoted - # by their test id) + # by their test id). if p.basename.startswith("conftest.py"): nodeid = p.dirpath().relto(self.config.rootdir) if p.sep != nodes.SEP: @@ -1518,7 +1520,7 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: self.parsefactories(plugin, nodeid) def _getautousenames(self, nodeid: str) -> List[str]: - """ return a tuple of fixture names to be used. """ + """Return a list of fixture names to be used.""" autousenames = [] # type: List[str] for baseid, basenames in self._nodeid_and_autousenames: if nodeid.startswith(baseid): @@ -1533,12 +1535,12 @@ def _getautousenames(self, nodeid: str) -> List[str]: def getfixtureclosure( self, fixturenames: Tuple[str, ...], parentnode, ignore_args: Sequence[str] = () ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef]]]: - # collect the closure of all fixtures , starting with the given + # Collect the closure of all fixtures, starting with the given # fixturenames as the initial set. As we have to visit all # factory definitions anyway, we also return an arg2fixturedefs # mapping so that the caller can reuse it and does not have # to re-discover fixturedefs again for each fixturename - # (discovering matching fixtures for a given name/node is expensive) + # (discovering matching fixtures for a given name/node is expensive). parentid = parentnode.nodeid fixturenames_closure = self._getautousenames(parentid) @@ -1550,7 +1552,7 @@ def merge(otherlist: Iterable[str]) -> None: merge(fixturenames) - # at this point, fixturenames_closure contains what we call "initialnames", + # At this point, fixturenames_closure contains what we call "initialnames", # which is a set of fixturenames the function immediately requests. We # need to return it as well, so save this. initialnames = tuple(fixturenames_closure) @@ -1608,10 +1610,10 @@ def pytest_generate_tests(self, metafunc: "Metafunc") -> None: ids=fixturedef.ids, ) else: - continue # will raise FixtureLookupError at setup time + continue # Will raise FixtureLookupError at setup time. def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None: - # separate parametrized setups + # Separate parametrized setups. items[:] = reorder_items(items) def parsefactories( @@ -1633,16 +1635,17 @@ def parsefactories( obj = safe_getattr(holderobj, name, None) marker = getfixturemarker(obj) if not isinstance(marker, FixtureFunctionMarker): - # magic globals with __getattr__ might have got us a wrong - # fixture attribute + # Magic globals with __getattr__ might have got us a wrong + # fixture attribute. continue if marker.name: name = marker.name - # during fixture definition we wrap the original fixture function - # to issue a warning if called directly, so here we unwrap it in order to not emit the warning - # when pytest itself calls the fixture function + # During fixture definition we wrap the original fixture function + # to issue a warning if called directly, so here we unwrap it in + # order to not emit the warning when pytest itself calls the + # fixture function. obj = get_real_method(obj, holderobj) fixture_def = FixtureDef( @@ -1675,12 +1678,11 @@ def parsefactories( def getfixturedefs( self, argname: str, nodeid: str ) -> Optional[Sequence[FixtureDef]]: - """ - Gets a list of fixtures which are applicable to the given node id. + """Get a list of fixtures which are applicable to the given node id. - :param str argname: name of the fixture to search for - :param str nodeid: full node id of the requesting test. - :return: list[FixtureDef] + :param str argname: Name of the fixture to search for. + :param str nodeid: Full node id of the requesting test. + :rtype: Sequence[FixtureDef] """ try: fixturedefs = self._arg2fixturedefs[argname] diff --git a/src/_pytest/freeze_support.py b/src/_pytest/freeze_support.py index 63c14ecebfa..8b93ed5f7f8 100644 --- a/src/_pytest/freeze_support.py +++ b/src/_pytest/freeze_support.py @@ -1,7 +1,5 @@ -""" -Provides a function to report all internal modules for using freezing tools -pytest -""" +"""Provides a function to report all internal modules for using freezing +tools.""" import types from typing import Iterator from typing import List @@ -9,10 +7,8 @@ def freeze_includes() -> List[str]: - """ - Returns a list of module names used by pytest that should be - included by cx_freeze. - """ + """Return a list of module names used by pytest that should be + included by cx_freeze.""" import py import _pytest @@ -24,8 +20,7 @@ def freeze_includes() -> List[str]: def _iter_all_modules( package: Union[str, types.ModuleType], prefix: str = "", ) -> Iterator[str]: - """ - Iterates over the names of all modules that can be found in the given + """Iterate over the names of all modules that can be found in the given package, recursively. >>> import _pytest diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index f3623b8a103..348a65edec6 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -1,4 +1,4 @@ -""" version info, help messages, tracing configuration. """ +"""Version info, help messages, tracing configuration.""" import os import sys from argparse import Action @@ -16,8 +16,9 @@ class HelpAction(Action): - """This is an argparse Action that will raise an exception in - order to skip the rest of the argument parsing when --help is passed. + """An argparse Action that will raise an exception in order to skip the + rest of the argument parsing when --help is passed. + This prevents argparse from quitting due to missing required arguments when any are defined, for example by ``pytest_addoption``. This is similar to the way that the builtin argparse --help option is @@ -37,7 +38,7 @@ def __init__(self, option_strings, dest=None, default=False, help=None): def __call__(self, parser, namespace, values, option_string=None): setattr(namespace, self.dest, self.const) - # We should only skip the rest of the parsing after preparse is done + # We should only skip the rest of the parsing after preparse is done. if getattr(parser._parser, "after_preparse", False): raise PrintHelp diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index d21c4d4d9ef..60b1b643aed 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,4 +1,5 @@ -""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ +"""Hook specifications for pytest plugins which are invoked by pytest itself +and by builtin plugins.""" from typing import Any from typing import Dict from typing import List @@ -51,11 +52,10 @@ @hookspec(historic=True) def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: - """called at plugin registration time to allow adding new hooks via a call to + """Called at plugin registration time to allow adding new hooks via a call to ``pluginmanager.add_hookspecs(module_or_class, prefix)``. - - :param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager + :param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager. .. note:: This hook is incompatible with ``hookwrapper=True``. @@ -66,10 +66,10 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: def pytest_plugin_registered( plugin: "_PluggyPlugin", manager: "PytestPluginManager" ) -> None: - """ a new pytest plugin got registered. + """A new pytest plugin got registered. - :param plugin: the plugin module or instance - :param _pytest.config.PytestPluginManager manager: pytest plugin manager + :param plugin: The plugin module or instance. + :param _pytest.config.PytestPluginManager manager: pytest plugin manager. .. note:: This hook is incompatible with ``hookwrapper=True``. @@ -78,7 +78,7 @@ def pytest_plugin_registered( @hookspec(historic=True) def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None: - """register argparse-style options and ini-style config values, + """Register argparse-style options and ini-style config values, called once at the beginning of a test run. .. note:: @@ -87,15 +87,16 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> files situated at the tests root directory due to how pytest :ref:`discovers plugins during startup `. - :arg _pytest.config.argparsing.Parser parser: To add command line options, call + :param _pytest.config.argparsing.Parser parser: + To add command line options, call :py:func:`parser.addoption(...) <_pytest.config.argparsing.Parser.addoption>`. To add ini-file values call :py:func:`parser.addini(...) <_pytest.config.argparsing.Parser.addini>`. - :arg _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager, - which can be used to install :py:func:`hookspec`'s or :py:func:`hookimpl`'s - and allow one plugin to call another plugin's hooks to change how - command line options are added. + :param _pytest.config.PytestPluginManager pluginmanager: + pytest plugin manager, which can be used to install :py:func:`hookspec`'s + or :py:func:`hookimpl`'s and allow one plugin to call another plugin's hooks + to change how command line options are added. Options can later be accessed through the :py:class:`config <_pytest.config.Config>` object, respectively: @@ -116,8 +117,7 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> @hookspec(historic=True) def pytest_configure(config: "Config") -> None: - """ - Allows plugins and conftest files to perform initial configuration. + """Allow plugins and conftest files to perform initial configuration. This hook is called for every plugin and initial conftest file after command line options have been parsed. @@ -128,7 +128,7 @@ def pytest_configure(config: "Config") -> None: .. note:: This hook is incompatible with ``hookwrapper=True``. - :arg _pytest.config.Config config: pytest config object + :param _pytest.config.Config config: The pytest config object. """ @@ -142,16 +142,17 @@ def pytest_configure(config: "Config") -> None: def pytest_cmdline_parse( pluginmanager: "PytestPluginManager", args: List[str] ) -> Optional["Config"]: - """return initialized config object, parsing the specified args. + """Return an initialized config object, parsing the specified args. - Stops at first non-None result, see :ref:`firstresult` + Stops at first non-None result, see :ref:`firstresult`. .. note:: - This hook will only be called for plugin classes passed to the ``plugins`` arg when using `pytest.main`_ to - perform an in-process test run. + This hook will only be called for plugin classes passed to the + ``plugins`` arg when using `pytest.main`_ to perform an in-process + test run. - :param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager - :param list[str] args: list of arguments passed on the command line + :param _pytest.config.PytestPluginManager pluginmanager: Pytest plugin manager. + :param List[str] args: List of arguments passed on the command line. """ @@ -164,37 +165,37 @@ def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None: .. note:: This hook will not be called for ``conftest.py`` files, only for setuptools plugins. - :param _pytest.config.Config config: pytest config object - :param list[str] args: list of arguments passed on the command line + :param _pytest.config.Config config: The pytest config object. + :param List[str] args: Arguments passed on the command line. """ @hookspec(firstresult=True) def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]: - """ called for performing the main command line action. The default + """Called for performing the main command line action. The default implementation will invoke the configure hooks and runtest_mainloop. .. note:: This hook will not be called for ``conftest.py`` files, only for setuptools plugins. - Stops at first non-None result, see :ref:`firstresult` + Stops at first non-None result, see :ref:`firstresult`. - :param _pytest.config.Config config: pytest config object + :param _pytest.config.Config config: The pytest config object. """ def pytest_load_initial_conftests( early_config: "Config", parser: "Parser", args: List[str] ) -> None: - """ implements the loading of initial conftest files ahead + """Called to implement the loading of initial conftest files ahead of command line option parsing. .. note:: This hook will not be called for ``conftest.py`` files, only for setuptools plugins. - :param _pytest.config.Config early_config: pytest config object - :param list[str] args: list of arguments passed on the command line - :param _pytest.config.argparsing.Parser parser: to add command line options + :param _pytest.config.Config early_config: The pytest config object. + :param List[str] args: Arguments passed on the command line. + :param _pytest.config.argparsing.Parser parser: To add command line options. """ @@ -224,26 +225,26 @@ def pytest_collection(session: "Session") -> Optional[object]: for example the terminal plugin uses it to start displaying the collection counter (and returns `None`). - :param _pytest.main.Session session: the pytest session object + :param _pytest.main.Session session: The pytest session object. """ def pytest_collection_modifyitems( session: "Session", config: "Config", items: List["Item"] ) -> None: - """ called after collection has been performed, may filter or re-order + """Called after collection has been performed. May filter or re-order the items in-place. - :param _pytest.main.Session session: the pytest session object - :param _pytest.config.Config config: pytest config object - :param List[_pytest.nodes.Item] items: list of item objects + :param _pytest.main.Session session: The pytest session object. + :param _pytest.config.Config config: The pytest config object. + :param List[_pytest.nodes.Item] items: List of item objects. """ def pytest_collection_finish(session: "Session") -> None: """Called after collection has been performed and modified. - :param _pytest.main.Session session: the pytest session object + :param _pytest.main.Session session: The pytest session object. """ @@ -256,8 +257,8 @@ def pytest_ignore_collect(path: py.path.local, config: "Config") -> Optional[boo Stops at first non-None result, see :ref:`firstresult`. - :param path: a :py:class:`py.path.local` - the path to analyze - :param _pytest.config.Config config: pytest config object + :param py.path.local path: The path to analyze. + :param _pytest.config.Config config: The pytest config object. """ @@ -267,7 +268,7 @@ def pytest_collect_directory(path: py.path.local, parent) -> Optional[object]: Stops at first non-None result, see :ref:`firstresult`. - :param path: a :py:class:`py.path.local` - the path to analyze + :param py.path.local path: The path to analyze. """ @@ -276,7 +277,7 @@ def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]": Any new node needs to have the specified ``parent`` as a parent. - :param path: a :py:class:`py.path.local` - the path to collect + :param py.path.local path: The path to collect. """ @@ -284,7 +285,7 @@ def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]": def pytest_collectstart(collector: "Collector") -> None: - """ collector starts collecting. """ + """Collector starts collecting.""" def pytest_itemcollected(item: "Item") -> None: @@ -292,7 +293,7 @@ def pytest_itemcollected(item: "Item") -> None: def pytest_collectreport(report: "CollectReport") -> None: - """ collector finished collecting. """ + """Collector finished collecting.""" def pytest_deselected(items: Sequence["Item"]) -> None: @@ -301,9 +302,10 @@ def pytest_deselected(items: Sequence["Item"]) -> None: @hookspec(firstresult=True) def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectReport]": - """ perform ``collector.collect()`` and return a CollectReport. + """Perform ``collector.collect()`` and return a CollectReport. - Stops at first non-None result, see :ref:`firstresult` """ + Stops at first non-None result, see :ref:`firstresult`. + """ # ------------------------------------------------------------------------- @@ -321,7 +323,7 @@ def pytest_pycollect_makemodule(path: py.path.local, parent) -> Optional["Module Stops at first non-None result, see :ref:`firstresult`. - :param path: a :py:class:`py.path.local` - the path of module to collect + :param py.path.local path: The path of module to collect. """ @@ -337,28 +339,31 @@ def pytest_pycollect_makeitem( @hookspec(firstresult=True) def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: - """ call underlying test function. + """Call underlying test function. - Stops at first non-None result, see :ref:`firstresult` """ + Stops at first non-None result, see :ref:`firstresult`. + """ def pytest_generate_tests(metafunc: "Metafunc") -> None: - """ generate (multiple) parametrized calls to a test function.""" + """Generate (multiple) parametrized calls to a test function.""" @hookspec(firstresult=True) def pytest_make_parametrize_id( config: "Config", val: object, argname: str ) -> Optional[str]: - """Return a user-friendly string representation of the given ``val`` that will be used - by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``. + """Return a user-friendly string representation of the given ``val`` + that will be used by @pytest.mark.parametrize calls, or None if the hook + doesn't know about ``val``. + The parameter name is available as ``argname``, if required. - Stops at first non-None result, see :ref:`firstresult` + Stops at first non-None result, see :ref:`firstresult`. - :param _pytest.config.Config config: pytest config object - :param val: the parametrized value - :param str argname: the automatic parameter name produced by pytest + :param _pytest.config.Config config: The pytest config object. + :param val: The parametrized value. + :param str argname: The automatic parameter name produced by pytest. """ @@ -369,7 +374,7 @@ def pytest_make_parametrize_id( @hookspec(firstresult=True) def pytest_runtestloop(session: "Session") -> Optional[object]: - """Performs the main runtest loop (after collection finished). + """Perform the main runtest loop (after collection finished). The default hook implementation performs the runtest protocol for all items collected in the session (``session.items``), unless the collection failed @@ -392,7 +397,7 @@ def pytest_runtestloop(session: "Session") -> Optional[object]: def pytest_runtest_protocol( item: "Item", nextitem: "Optional[Item]" ) -> Optional[object]: - """Performs the runtest protocol for a single test item. + """Perform the runtest protocol for a single test item. The default runtest protocol is this (see individual hooks for full details): @@ -418,9 +423,8 @@ def pytest_runtest_protocol( - ``pytest_runtest_logfinish(nodeid, location)`` - :arg item: Test item for which the runtest protocol is performed. - - :arg nextitem: The scheduled-to-be-next test item (or None if this is the end my friend). + :param item: Test item for which the runtest protocol is performed. + :param nextitem: The scheduled-to-be-next test item (or None if this is the end my friend). Stops at first non-None result, see :ref:`firstresult`. The return value is not used, but only stops further processing. @@ -476,10 +480,11 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None: includes running the teardown phase of fixtures required by the item (if they go out of scope). - :arg nextitem: The scheduled-to-be-next test item (None if no further - test item is scheduled). This argument can be used to - perform exact teardowns, i.e. calling just enough finalizers - so that nextitem only needs to call setup-functions. + :param nextitem: + The scheduled-to-be-next test item (None if no further test item is + scheduled). This argument can be used to perform exact teardowns, + i.e. calling just enough finalizers so that nextitem only needs to + call setup-functions. """ @@ -510,19 +515,15 @@ def pytest_runtest_logreport(report: "TestReport") -> None: def pytest_report_to_serializable( config: "Config", report: Union["CollectReport", "TestReport"], ) -> Optional[Dict[str, Any]]: - """ - Serializes the given report object into a data structure suitable for sending - over the wire, e.g. converted to JSON. - """ + """Serialize the given report object into a data structure suitable for + sending over the wire, e.g. converted to JSON.""" @hookspec(firstresult=True) def pytest_report_from_serializable( config: "Config", data: Dict[str, Any], ) -> Optional[Union["CollectReport", "TestReport"]]: - """ - Restores a report object previously serialized with pytest_report_to_serializable(). - """ + """Restore a report object previously serialized with pytest_report_to_serializable().""" # ------------------------------------------------------------------------- @@ -534,9 +535,9 @@ def pytest_report_from_serializable( def pytest_fixture_setup( fixturedef: "FixtureDef", request: "SubRequest" ) -> Optional[object]: - """Performs fixture setup execution. + """Perform fixture setup execution. - :return: The return value of the call to the fixture function. + :returns: The return value of the call to the fixture function. Stops at first non-None result, see :ref:`firstresult`. @@ -564,7 +565,7 @@ def pytest_sessionstart(session: "Session") -> None: """Called after the ``Session`` object has been created and before performing collection and entering the run test loop. - :param _pytest.main.Session session: the pytest session object + :param _pytest.main.Session session: The pytest session object. """ @@ -573,15 +574,15 @@ def pytest_sessionfinish( ) -> None: """Called after whole test run finished, right before returning the exit status to the system. - :param _pytest.main.Session session: the pytest session object - :param int exitstatus: the status which pytest will return to the system + :param _pytest.main.Session session: The pytest session object. + :param int exitstatus: The status which pytest will return to the system. """ def pytest_unconfigure(config: "Config") -> None: """Called before test process is exited. - :param _pytest.config.Config config: pytest config object + :param _pytest.config.Config config: The pytest config object. """ @@ -596,22 +597,19 @@ def pytest_assertrepr_compare( """Return explanation for comparisons in failing assert expressions. Return None for no custom explanation, otherwise return a list - of strings. The strings will be joined by newlines but any newlines - *in* a string will be escaped. Note that all but the first line will + of strings. The strings will be joined by newlines but any newlines + *in* a string will be escaped. Note that all but the first line will be indented slightly, the intention is for the first line to be a summary. - :param _pytest.config.Config config: pytest config object + :param _pytest.config.Config config: The pytest config object. """ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> None: - """ - **(Experimental)** + """**(Experimental)** Called whenever an assertion passes. .. versionadded:: 5.0 - Hook called whenever an assertion *passes*. - Use this hook to do some processing after a passing assertion. The original assertion information is available in the `orig` string and the pytest introspected assertion information is available in the @@ -628,32 +626,32 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No You need to **clean the .pyc** files in your project directory and interpreter libraries when enabling this option, as assertions will require to be re-written. - :param _pytest.nodes.Item item: pytest item object of current test - :param int lineno: line number of the assert statement - :param string orig: string with original assertion - :param string expl: string with assert explanation + :param _pytest.nodes.Item item: pytest item object of current test. + :param int lineno: Line number of the assert statement. + :param str orig: String with the original assertion. + :param str expl: String with the assert explanation. .. note:: This hook is **experimental**, so its parameters or even the hook itself might be changed/removed without warning in any future pytest release. - If you find this hook useful, please share your feedback opening an issue. + If you find this hook useful, please share your feedback in an issue. """ # ------------------------------------------------------------------------- -# hooks for influencing reporting (invoked from _pytest_terminal) +# Hooks for influencing reporting (invoked from _pytest_terminal). # ------------------------------------------------------------------------- def pytest_report_header( config: "Config", startdir: py.path.local ) -> Union[str, List[str]]: - """ return a string or list of strings to be displayed as header info for terminal reporting. + """Return a string or list of strings to be displayed as header info for terminal reporting. - :param _pytest.config.Config config: pytest config object - :param startdir: py.path object with the starting dir + :param _pytest.config.Config config: The pytest config object. + :param py.path.local startdir: The starting dir. .. note:: @@ -673,16 +671,16 @@ def pytest_report_header( def pytest_report_collectionfinish( config: "Config", startdir: py.path.local, items: Sequence["Item"], ) -> Union[str, List[str]]: - """ - .. versionadded:: 3.2 - - Return a string or list of strings to be displayed after collection has finished successfully. + """Return a string or list of strings to be displayed after collection + has finished successfully. These strings will be displayed after the standard "collected X items" message. - :param _pytest.config.Config config: pytest config object - :param startdir: py.path object with the starting dir - :param items: list of pytest items that are going to be executed; this list should not be modified. + .. versionadded:: 3.2 + + :param _pytest.config.Config config: The pytest config object. + :param py.path.local startdir: The starting dir. + :param items: List of pytest items that are going to be executed; this list should not be modified. .. note:: @@ -727,9 +725,9 @@ def pytest_terminal_summary( ) -> None: """Add a section to terminal summary reporting. - :param _pytest.terminal.TerminalReporter terminalreporter: the internal terminal reporter object - :param int exitstatus: the exit status that will be reported back to the OS - :param _pytest.config.Config config: pytest config object + :param _pytest.terminal.TerminalReporter terminalreporter: The internal terminal reporter object. + :param int exitstatus: The exit status that will be reported back to the OS. + :param _pytest.config.Config config: The pytest config object. .. versionadded:: 4.2 The ``config`` parameter. @@ -780,8 +778,7 @@ def pytest_warning_recorded( nodeid: str, location: Optional[Tuple[str, int, str]], ) -> None: - """ - Process a warning captured by the internal pytest warnings plugin. + """Process a warning captured by the internal pytest warnings plugin. :param warnings.WarningMessage warning_message: The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains @@ -794,7 +791,8 @@ def pytest_warning_recorded( * ``"collect"``: during test collection. * ``"runtest"``: during test execution. - :param str nodeid: full id of the item + :param str nodeid: + Full id of the item. :param tuple|None location: When available, holds information about the execution context of the captured @@ -823,7 +821,7 @@ def pytest_internalerror( def pytest_keyboard_interrupt( excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]", ) -> None: - """ called for keyboard interrupt. """ + """Called for keyboard interrupt.""" def pytest_exception_interact( @@ -846,20 +844,22 @@ def pytest_exception_interact( def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None: - """ called upon pdb.set_trace(), can be used by plugins to take special - action just before the python debugger enters in interactive mode. + """Called upon pdb.set_trace(). + + Can be used by plugins to take special action just before the python + debugger enters interactive mode. - :param _pytest.config.Config config: pytest config object - :param pdb.Pdb pdb: Pdb instance + :param _pytest.config.Config config: The pytest config object. + :param pdb.Pdb pdb: The Pdb instance. """ def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None: - """ called when leaving pdb (e.g. with continue after pdb.set_trace()). + """Called when leaving pdb (e.g. with continue after pdb.set_trace()). Can be used by plugins to take special action just after the python debugger leaves interactive mode. - :param _pytest.config.Config config: pytest config object - :param pdb.Pdb pdb: Pdb instance + :param _pytest.config.Config config: The pytest config object. + :param pdb.Pdb pdb: The Pdb instance. """ diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 28ae69e82ac..6e3785b7d6f 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -1,12 +1,10 @@ -""" - report test results in JUnit-XML format, - for use with Jenkins and build integration servers. - +"""Report test results in JUnit-XML format, for use with Jenkins and build +integration servers. Based on initial code from Ross Lawley. -Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/ -src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd +Output conforms to +https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd """ import functools import os @@ -81,11 +79,11 @@ def merge_family(left, right) -> None: families["_base"] = {"testcase": ["classname", "name"]} families["_base_legacy"] = {"testcase": ["file", "line", "url"]} -# xUnit 1.x inherits legacy attributes +# xUnit 1.x inherits legacy attributes. families["xunit1"] = families["_base"].copy() merge_family(families["xunit1"], families["_base_legacy"]) -# xUnit 2.x uses strict base attributes +# xUnit 2.x uses strict base attributes. families["xunit2"] = families["_base"] @@ -111,8 +109,7 @@ def add_attribute(self, name: str, value: object) -> None: self.attrs[str(name)] = bin_xml_escape(value) def make_properties_node(self) -> Optional[ET.Element]: - """Return a Junit node containing custom properties, if any. - """ + """Return a Junit node containing custom properties, if any.""" if self.properties: properties = ET.Element("properties") for name, value in self.properties: @@ -136,9 +133,9 @@ def record_testreport(self, testreport: TestReport) -> None: if hasattr(testreport, "url"): attrs["url"] = testreport.url self.attrs = attrs - self.attrs.update(existing_attrs) # restore any user-defined attributes + self.attrs.update(existing_attrs) # Restore any user-defined attributes. - # Preserve legacy testcase behavior + # Preserve legacy testcase behavior. if self.family == "xunit1": return @@ -262,7 +259,7 @@ def finalize(self) -> None: def _warn_incompatibility_with_xunit2( request: FixtureRequest, fixture_name: str ) -> None: - """Emits a PytestWarning about the given fixture being incompatible with newer xunit revisions""" + """Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions.""" from _pytest.warning_types import PytestWarning xml = request.config._store.get(xml_key, None) @@ -330,7 +327,7 @@ def add_attr_noop(name: str, value: object) -> None: def _check_record_param_type(param: str, v: str) -> None: """Used by record_testsuite_property to check that the given parameter name is of the proper - type""" + type.""" __tracebackhide__ = True if not isinstance(v, str): msg = "{param} parameter needs to be a string, but {g} given" @@ -339,9 +336,10 @@ def _check_record_param_type(param: str, v: str) -> None: @pytest.fixture(scope="session") def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]: - """ - Records a new ```` tag as child of the root ````. This is suitable to - writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family. + """Record a new ```` tag as child of the root ````. + + This is suitable to writing global information regarding the entire test + suite, and is compatible with ``xunit2`` JUnit family. This is a ``session``-scoped fixture which is called with ``(name, value)``. Example: @@ -357,7 +355,7 @@ def test_foo(record_testsuite_property): __tracebackhide__ = True def record_func(name: str, value: object) -> None: - """noop function in case --junitxml was not passed in the command-line""" + """No-op function in case --junitxml was not passed in the command-line.""" __tracebackhide__ = True _check_record_param_type("name", name) @@ -414,7 +412,7 @@ def pytest_addoption(parser: Parser) -> None: def pytest_configure(config: Config) -> None: xmlpath = config.option.xmlpath - # prevent opening xmllog on worker nodes (xdist) + # Prevent opening xmllog on worker nodes (xdist). if xmlpath and not hasattr(config, "workerinput"): junit_family = config.getini("junit_family") if not junit_family: @@ -446,10 +444,10 @@ def mangle_test_address(address: str) -> List[str]: names.remove("()") except ValueError: pass - # convert file path to dotted path + # Convert file path to dotted path. names[0] = names[0].replace(nodes.SEP, ".") names[0] = re.sub(r"\.py$", "", names[0]) - # put any params back + # Put any params back. names[-1] += possible_open_bracket + params return names @@ -486,13 +484,13 @@ def __init__( self.open_reports = [] # type: List[TestReport] self.cnt_double_fail_tests = 0 - # Replaces convenience family with real family + # Replaces convenience family with real family. if self.family == "legacy": self.family = "xunit1" def finalize(self, report: TestReport) -> None: nodeid = getattr(report, "nodeid", report) - # local hack to handle xdist report order + # Local hack to handle xdist report order. workernode = getattr(report, "node", None) reporter = self.node_reporters.pop((nodeid, workernode)) if reporter is not None: @@ -500,7 +498,7 @@ def finalize(self, report: TestReport) -> None: def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter: nodeid = getattr(report, "nodeid", report) # type: Union[str, TestReport] - # local hack to handle xdist report order + # Local hack to handle xdist report order. workernode = getattr(report, "node", None) key = nodeid, workernode @@ -526,13 +524,13 @@ def _opentestcase(self, report: TestReport) -> _NodeReporter: return reporter def pytest_runtest_logreport(self, report: TestReport) -> None: - """handle a setup/call/teardown report, generating the appropriate - xml tags as necessary. + """Handle a setup/call/teardown report, generating the appropriate + XML tags as necessary. - note: due to plugins like xdist, this hook may be called in interlaced - order with reports from other nodes. for example: + Note: due to plugins like xdist, this hook may be called in interlaced + order with reports from other nodes. For example: - usual call order: + Usual call order: -> setup node1 -> call node1 -> teardown node1 @@ -540,7 +538,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: -> call node2 -> teardown node2 - possible call order in xdist: + Possible call order in xdist: -> setup node1 -> call node1 -> setup node2 @@ -555,7 +553,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: reporter.append_pass(report) elif report.failed: if report.when == "teardown": - # The following vars are needed when xdist plugin is used + # The following vars are needed when xdist plugin is used. report_wid = getattr(report, "worker_id", None) report_ii = getattr(report, "item_index", None) close_report = next( @@ -573,7 +571,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: if close_report: # We need to open new testcase in case we have failure in # call and error in teardown in order to follow junit - # schema + # schema. self.finalize(close_report) self.cnt_double_fail_tests += 1 reporter = self._opentestcase(report) @@ -614,9 +612,8 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: self.open_reports.remove(close_report) def update_testcase_duration(self, report: TestReport) -> None: - """accumulates total duration for nodeid from given report and updates - the Junit.testcase with the new total if already created. - """ + """Accumulate total duration for nodeid from given report and update + the Junit.testcase with the new total if already created.""" if self.report_duration == "total" or report.when == self.report_duration: reporter = self.node_reporter(report) reporter.duration += getattr(report, "duration", 0.0) @@ -684,8 +681,7 @@ def add_global_property(self, name: str, value: object) -> None: self.global_properties.append((name, bin_xml_escape(value))) def _get_global_properties_node(self) -> Optional[ET.Element]: - """Return a Junit node containing custom properties, if any. - """ + """Return a Junit node containing custom properties, if any.""" if self.global_properties: properties = ET.Element("properties") for name, value in self.global_properties: diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 0ee9457ea72..5dfd47887a0 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -1,4 +1,4 @@ -""" Access and control log capturing. """ +"""Access and control log capturing.""" import logging import os import re @@ -43,9 +43,8 @@ def _remove_ansi_escape_sequences(text: str) -> str: class ColoredLevelFormatter(logging.Formatter): - """ - Colorize the %(levelname)..s part of the log format passed to __init__. - """ + """A logging formatter which colorizes the %(levelname)..s part of the + log format passed to __init__.""" LOGLEVEL_COLOROPTS = { logging.CRITICAL: {"red"}, @@ -110,7 +109,7 @@ def _update_message( @staticmethod def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int: - """Determines the current auto indentation setting + """Determine the current auto indentation setting. Specify auto indent behavior (on/off/fixed) by passing in extra={"auto_indent": [value]} to the call to logging.log() or @@ -128,12 +127,14 @@ def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int: Any other values for the option are invalid, and will silently be converted to the default. - :param any auto_indent_option: User specified option for indentation - from command line, config or extra kwarg. Accepts int, bool or str. - str option accepts the same range of values as boolean config options, - as well as positive integers represented in str form. + :param None|bool|int|str auto_indent_option: + User specified option for indentation from command line, config + or extra kwarg. Accepts int, bool or str. str option accepts the + same range of values as boolean config options, as well as + positive integers represented in str form. - :returns: indentation value, which can be + :returns: + Indentation value, which can be -1 (automatically determine indentation) or 0 (auto-indent turned off) or >0 (explicitly set indentation position). @@ -164,7 +165,7 @@ def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int: def format(self, record: logging.LogRecord) -> str: if "\n" in record.message: if hasattr(record, "auto_indent"): - # passed in from the "extra={}" kwarg on the call to logging.log() + # Passed in from the "extra={}" kwarg on the call to logging.log(). auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined] else: auto_indent = self._auto_indent @@ -178,7 +179,7 @@ def format(self, record: logging.LogRecord) -> str: lines[0] ) else: - # optimizes logging by allowing a fixed indentation + # Optimizes logging by allowing a fixed indentation. indentation = auto_indent lines[0] = formatted return ("\n" + " " * indentation).join(lines) @@ -316,7 +317,7 @@ class LogCaptureHandler(logging.StreamHandler): stream = None # type: StringIO def __init__(self) -> None: - """Creates a new log handler.""" + """Create a new log handler.""" super().__init__(StringIO()) self.records = [] # type: List[logging.LogRecord] @@ -342,18 +343,17 @@ class LogCaptureFixture: """Provides access and control of log capturing.""" def __init__(self, item: nodes.Node) -> None: - """Creates a new funcarg.""" self._item = item - # dict of log name -> log level self._initial_handler_level = None # type: Optional[int] + # Dict of log name -> log level. self._initial_logger_levels = {} # type: Dict[Optional[str], int] def _finalize(self) -> None: - """Finalizes the fixture. + """Finalize the fixture. This restores the log levels changed by :meth:`set_level`. """ - # restore log levels + # Restore log levels. if self._initial_handler_level is not None: self.handler.setLevel(self._initial_handler_level) for logger_name, level in self._initial_logger_levels.items(): @@ -362,20 +362,20 @@ def _finalize(self) -> None: @property def handler(self) -> LogCaptureHandler: - """ + """Get the logging handler used by the fixture. + :rtype: LogCaptureHandler """ return self._item._store[caplog_handler_key] def get_records(self, when: str) -> List[logging.LogRecord]: - """ - Get the logging records for one of the possible test phases. + """Get the logging records for one of the possible test phases. :param str when: Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown". + :returns: The list of captured records at the given stage. :rtype: List[logging.LogRecord] - :return: the list of captured records at the given stage .. versionadded:: 3.4 """ @@ -383,17 +383,17 @@ def get_records(self, when: str) -> List[logging.LogRecord]: @property def text(self) -> str: - """Returns the formatted log text.""" + """The formatted log text.""" return _remove_ansi_escape_sequences(self.handler.stream.getvalue()) @property def records(self) -> List[logging.LogRecord]: - """Returns the list of log records.""" + """The list of log records.""" return self.handler.records @property def record_tuples(self) -> List[Tuple[str, int, str]]: - """Returns a list of a stripped down version of log records intended + """A list of a stripped down version of log records intended for use in assertion comparison. The format of the tuple is: @@ -404,15 +404,18 @@ def record_tuples(self) -> List[Tuple[str, int, str]]: @property def messages(self) -> List[str]: - """Returns a list of format-interpolated log messages. + """A list of format-interpolated log messages. + + Unlike 'records', which contains the format string and parameters for + interpolation, log messages in this list are all interpolated. - Unlike 'records', which contains the format string and parameters for interpolation, log messages in this list - are all interpolated. - Unlike 'text', which contains the output from the handler, log messages in this list are unadorned with - levels, timestamps, etc, making exact comparisons more reliable. + Unlike 'text', which contains the output from the handler, log + messages in this list are unadorned with levels, timestamps, etc, + making exact comparisons more reliable. - Note that traceback or stack info (from :func:`logging.exception` or the `exc_info` or `stack_info` arguments - to the logging functions) is not included, as this is added by the formatter in the handler. + Note that traceback or stack info (from :func:`logging.exception` or + the `exc_info` or `stack_info` arguments to the logging functions) is + not included, as this is added by the formatter in the handler. .. versionadded:: 3.7 """ @@ -423,18 +426,17 @@ def clear(self) -> None: self.handler.reset() def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None: - """Sets the level for capturing of logs. The level will be restored to its previous value at the end of - the test. - - :param int level: the logger to level. - :param str logger: the logger to update the level. If not given, the root logger level is updated. + """Set the level of a logger for the duration of a test. .. versionchanged:: 3.4 - The levels of the loggers changed by this function will be restored to their initial values at the - end of the test. + The levels of the loggers changed by this function will be + restored to their initial values at the end of the test. + + :param int level: The level. + :param str logger: The logger to update. If not given, the root logger. """ logger_obj = logging.getLogger(logger) - # save the original log-level to restore it during teardown + # Save the original log-level to restore it during teardown. self._initial_logger_levels.setdefault(logger, logger_obj.level) logger_obj.setLevel(level) self._initial_handler_level = self.handler.level @@ -444,11 +446,12 @@ def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> Non def at_level( self, level: int, logger: Optional[str] = None ) -> Generator[None, None, None]: - """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the - level is restored to its original value. + """Context manager that sets the level for capturing of logs. After + the end of the 'with' statement the level is restored to its original + value. - :param int level: the logger to level. - :param str logger: the logger to update the level. If not given, the root logger level is updated. + :param int level: The level. + :param str logger: The logger to update. If not given, the root logger. """ logger_obj = logging.getLogger(logger) orig_level = logger_obj.level @@ -509,11 +512,10 @@ def pytest_configure(config: Config) -> None: class LoggingPlugin: - """Attaches to the logging module and captures log messages for each test. - """ + """Attaches to the logging module and captures log messages for each test.""" def __init__(self, config: Config) -> None: - """Creates a new plugin to capture log messages. + """Create a new plugin to capture log messages. The formatter can be safely shared across all handlers so create a single one for the entire test session here. @@ -572,7 +574,7 @@ def __init__(self, config: Config) -> None: self.log_cli_handler.setFormatter(log_cli_formatter) def _create_formatter(self, log_format, log_date_format, auto_indent): - # color option doesn't exist if terminal plugin is disabled + # Color option doesn't exist if terminal plugin is disabled. color = getattr(self._config.option, "color", "no") if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search( log_format @@ -590,12 +592,12 @@ def _create_formatter(self, log_format, log_date_format, auto_indent): return formatter def set_log_path(self, fname: str) -> None: - """Public method, which can set filename parameter for - Logging.FileHandler(). Also creates parent directory if - it does not exist. + """Set the filename parameter for Logging.FileHandler(). + + Creates parent directory if it does not exist. .. warning:: - Please considered as an experimental API. + This is an experimental API. """ fpath = Path(fname) @@ -652,19 +654,17 @@ def pytest_collection(self) -> Generator[None, None, None]: @pytest.hookimpl(hookwrapper=True) def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]: - """Runs all collected test items.""" - if session.config.option.collectonly: yield return if self._log_cli_enabled() and self._config.getoption("verbose") < 1: - # setting verbose flag is needed to avoid messy test progress output + # The verbose flag is needed to avoid messy test progress output. self._config.option.verbose = 1 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 + yield # Run all the tests. @pytest.hookimpl def pytest_runtest_logstart(self) -> None: @@ -676,7 +676,7 @@ def pytest_runtest_logreport(self) -> None: self.log_cli_handler.set_when("logreport") def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]: - """Implements the internals of pytest_runtest_xxx() hook.""" + """Implement the internals of the pytest_runtest_xxx() hooks.""" with catching_logs( self.caplog_handler, level=self.log_level, ) as caplog_handler, catching_logs( @@ -734,9 +734,7 @@ def pytest_unconfigure(self) -> None: class _FileHandler(logging.FileHandler): - """ - Custom FileHandler with pytest tweaks. - """ + """A logging FileHandler with pytest tweaks.""" def handleError(self, record: logging.LogRecord) -> None: # Handled by LogCaptureHandler. @@ -744,12 +742,12 @@ def handleError(self, record: logging.LogRecord) -> None: class _LiveLoggingStreamHandler(logging.StreamHandler): - """ - Custom StreamHandler used by the live logging feature: it will write a newline before the first log message - in each test. + """A logging StreamHandler used by the live logging feature: it will + write a newline before the first log message in each test. - During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured - and won't appear in the terminal. + During live logging we must also explicitly disable stdout/stderr + capturing otherwise it will get captured and won't appear in the + terminal. """ # Officially stream needs to be a IO[str], but TerminalReporter @@ -761,10 +759,6 @@ def __init__( terminal_reporter: TerminalReporter, capture_manager: Optional[CaptureManager], ) -> None: - """ - :param _pytest.terminal.TerminalReporter terminal_reporter: - :param _pytest.capture.CaptureManager capture_manager: - """ logging.StreamHandler.__init__(self, stream=terminal_reporter) # type: ignore[arg-type] self.capture_manager = capture_manager self.reset() @@ -772,11 +766,11 @@ def __init__( self._test_outcome_written = False def reset(self) -> None: - """Reset the handler; should be called before the start of each test""" + """Reset the handler; should be called before the start of each test.""" self._first_record_emitted = False def set_when(self, when: Optional[str]) -> None: - """Prepares for the given test phase (setup/call/teardown)""" + """Prepare for the given test phase (setup/call/teardown).""" self._when = when self._section_name_shown = False if when == "start": @@ -807,7 +801,7 @@ def handleError(self, record: logging.LogRecord) -> None: class _LiveLoggingNullHandler(logging.NullHandler): - """A handler used when live logging is disabled.""" + """A logging handler used when live logging is disabled.""" def reset(self) -> None: pass diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 96998830548..292ba58e24b 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -1,4 +1,4 @@ -""" core implementation of testing process: init, session, runtest loop. """ +"""Core implementation of the testing process: init, session, runtest loop.""" import argparse import fnmatch import functools @@ -206,7 +206,7 @@ def validate_basetemp(path: str) -> str: raise argparse.ArgumentTypeError(msg) def is_ancestor(base: Path, query: Path) -> bool: - """ return True if query is an ancestor of base, else False.""" + """Return whether query is an ancestor of base.""" if base == query: return True for parent in base.parents: @@ -228,7 +228,7 @@ def is_ancestor(base: Path, query: Path) -> bool: def wrap_session( config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]] ) -> Union[int, ExitCode]: - """Skeleton command line program""" + """Skeleton command line program.""" session = Session.from_config(config) session.exitstatus = ExitCode.OK initstate = 0 @@ -291,8 +291,8 @@ def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]: def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]: - """ default command line protocol for initialization, session, - running tests and reporting. """ + """Default command line protocol for initialization, session, + running tests and reporting.""" config.hook.pytest_collection(session=session) config.hook.pytest_runtestloop(session=session) @@ -328,8 +328,8 @@ def pytest_runtestloop(session: "Session") -> bool: def _in_venv(path: py.path.local) -> bool: - """Attempts to detect if ``path`` is the root of a Virtual Environment by - checking for the existence of the appropriate activate script""" + """Attempt to detect if ``path`` is the root of a Virtual Environment by + checking for the existence of the appropriate activate script.""" bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin") if not bindir.isdir(): return False @@ -390,17 +390,17 @@ def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> No class NoMatch(Exception): - """ raised if matching cannot locate a matching names. """ + """Matching cannot locate matching names.""" class Interrupted(KeyboardInterrupt): - """ signals an interrupted test run. """ + """Signals that the test run was interrupted.""" - __module__ = "builtins" # for py3 + __module__ = "builtins" # For py3. class Failed(Exception): - """ signals a stop as failed test run. """ + """Signals a stop as failed test run.""" @attr.s @@ -434,7 +434,7 @@ def __init__(self, config: Config) -> None: self.startdir = config.invocation_dir self._initialpaths = frozenset() # type: FrozenSet[py.path.local] - # Keep track of any collected nodes in here, so we don't duplicate fixtures + # Keep track of any collected nodes in here, so we don't duplicate fixtures. self._collection_node_cache1 = ( {} ) # type: Dict[py.path.local, Sequence[nodes.Collector]] @@ -469,7 +469,7 @@ def __repr__(self) -> str: ) def _node_location_to_relpath(self, node_path: py.path.local) -> str: - # bestrelpath is a quite slow function + # bestrelpath is a quite slow function. return self._bestrelpathcache[node_path] @hookimpl(tryfirst=True) @@ -594,7 +594,7 @@ def _collect( # Start with a Session root, and delve to argpath item (dir or file) # and stack all Packages found on the way. - # No point in finding packages when collecting doctests + # No point in finding packages when collecting doctests. if not self.config.getoption("doctestmodules", False): pm = self.config.pluginmanager for parent in reversed(argpath.parts()): @@ -609,7 +609,7 @@ def _collect( if col: if isinstance(col[0], Package): self._collection_pkg_roots[str(parent)] = col[0] - # always store a list in the cache, matchnodes expects it + # Always store a list in the cache, matchnodes expects it. self._collection_node_cache1[col[0].fspath] = [col[0]] # If it's a directory argument, recurse and look for any Subpackages. @@ -689,7 +689,7 @@ def _tryconvertpyarg(self, x: str) -> str: return spec.origin def _parsearg(self, arg: str) -> Tuple[py.path.local, List[str]]: - """ return (fspath, names) tuple after checking the file exists. """ + """Return (fspath, names) tuple after checking the file exists.""" strpath, *parts = str(arg).split("::") if self.config.option.pyargs: strpath = self._tryconvertpyarg(strpath) @@ -740,18 +740,18 @@ def _matchnodes( if rep.passed: has_matched = False for x in rep.result: - # TODO: remove parametrized workaround once collection structure contains parametrization + # TODO: Remove parametrized workaround once collection structure contains parametrization. if x.name == name or x.name.split("[")[0] == name: resultnodes.extend(self.matchnodes([x], nextnames)) has_matched = True - # XXX accept IDs that don't have "()" for class instances + # XXX Accept IDs that don't have "()" for class instances. if not has_matched and len(rep.result) == 1 and x.name == "()": nextnames.insert(0, name) resultnodes.extend(self.matchnodes([x], nextnames)) else: - # report collection failures here to avoid failing to run some test + # Report collection failures here to avoid failing to run some test # specified in the command line because the module could not be - # imported (#134) + # imported (#134). node.ihook.pytest_collectreport(report=rep) return resultnodes diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index bc1dd1a709c..d677d49c132 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -1,4 +1,4 @@ -""" generic mechanism for marking and selecting python functions. """ +"""Generic mechanism for marking and selecting python functions.""" import typing from typing import AbstractSet from typing import List @@ -58,9 +58,9 @@ def param( def test_eval(test_input, expected): assert eval(test_input) == expected - :param values: variable args of the values of the parameter set, in order. - :keyword marks: a single mark or a list of marks to be applied to this parameter set. - :keyword str id: the id to attribute to this parameter set. + :param values: Variable args of the values of the parameter set, in order. + :keyword marks: A single mark or a list of marks to be applied to this parameter set. + :keyword str id: The id to attribute to this parameter set. """ return ParameterSet.param(*values, marks=marks, id=id) @@ -148,22 +148,22 @@ class KeywordMatcher: def from_item(cls, item: "Item") -> "KeywordMatcher": mapped_names = set() - # Add the names of the current item and any parent items + # Add the names of the current item and any parent items. import pytest for node in item.listchain(): if not isinstance(node, (pytest.Instance, pytest.Session)): mapped_names.add(node.name) - # Add the names added as extra keywords to current or parent items + # Add the names added as extra keywords to current or parent items. mapped_names.update(item.listextrakeywords()) - # Add the names attached to the current function through direct assignment + # Add the names attached to the current function through direct assignment. function_obj = getattr(item, "function", None) if function_obj: mapped_names.update(function_obj.__dict__) - # add the markers to the keywords as we no longer handle them correctly + # Add the markers to the keywords as we no longer handle them correctly. mapped_names.update(mark.name for mark in item.iter_markers()) return cls(mapped_names) diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 73b7bf16992..f5700109757 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -1,5 +1,4 @@ -r""" -Evaluate match expressions, as used by `-k` and `-m`. +r"""Evaluate match expressions, as used by `-k` and `-m`. The grammar is: @@ -213,10 +212,11 @@ def compile(self, input: str) -> "Expression": def evaluate(self, matcher: Callable[[str], bool]) -> bool: """Evaluate the match expression. - :param matcher: Given an identifier, should return whether it matches or not. - Should be prepared to handle arbitrary strings as input. + :param matcher: + Given an identifier, should return whether it matches or not. + Should be prepared to handle arbitrary strings as input. - Returns whether the expression matches or not. + :returns: Whether the expression matches or not. """ ret = eval( self.code, {"__builtins__": {}}, MatcherAdapter(matcher) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 6567822999a..5abe4b94532 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -107,14 +107,15 @@ def extract_from( parameterset: Union["ParameterSet", Sequence[object], object], force_tuple: bool = False, ) -> "ParameterSet": - """ + """Extract from an object or objects. + :param parameterset: - a legacy style parameterset that may or may not be a tuple, - and may or may not be wrapped into a mess of mark objects + A legacy style parameterset that may or may not be a tuple, + and may or may not be wrapped into a mess of mark objects. :param force_tuple: - enforce tuple wrapping so single argument tuple values - don't get decomposed and break tests + Enforce tuple wrapping so single argument tuple values + don't get decomposed and break tests. """ if isinstance(parameterset, cls): @@ -166,7 +167,7 @@ def _for_parametrize( del argvalues if parameters: - # check all parameter sets have the correct number of values + # Check all parameter sets have the correct number of values. for param in parameters: if len(param.values) != len(argnames): msg = ( @@ -186,8 +187,8 @@ def _for_parametrize( pytrace=False, ) else: - # empty parameter set (likely computed at runtime): create a single - # parameter set with NOTSET values, with the "empty parameter set" mark applied to it + # Empty parameter set (likely computed at runtime): create a single + # parameter set with NOTSET values, with the "empty parameter set" mark applied to it. mark = get_empty_parameterset_mark(config, argnames, func) parameters.append( ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None) @@ -220,8 +221,7 @@ def combined_with(self, other: "Mark") -> "Mark": Combines by appending args and merging kwargs. - :param other: The mark to combine with. - :type other: Mark + :param Mark other: The mark to combine with. :rtype: Mark """ assert self.name == other.name @@ -314,7 +314,7 @@ def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": Unlike calling the MarkDecorator, with_args() can be used even if the sole argument is a callable/class. - :return: MarkDecorator + :rtype: MarkDecorator """ mark = Mark(self.name, args, kwargs) return self.__class__(self.mark.combined_with(mark)) @@ -344,9 +344,7 @@ def __call__(self, *args: object, **kwargs: object): # noqa: F811 def get_unpacked_marks(obj) -> List[Mark]: - """ - obtain the unpacked marks that are stored on an object - """ + """Obtain the unpacked marks that are stored on an object.""" mark_list = getattr(obj, "pytestmark", []) if not isinstance(mark_list, list): mark_list = [mark_list] @@ -354,10 +352,9 @@ def get_unpacked_marks(obj) -> List[Mark]: def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]: - """ - normalizes marker decorating helpers to mark objects + """Normalize marker decorating helpers to mark objects. - :type mark_list: List[Union[Mark, Markdecorator]] + :type List[Union[Mark, Markdecorator]] mark_list: :rtype: List[Mark] """ extracted = [ diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 2e5cca52628..19208ac6630 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -1,4 +1,4 @@ -""" monkeypatching and mocking functionality. """ +"""Monkeypatching and mocking functionality.""" import os import re import sys @@ -27,8 +27,10 @@ @fixture def monkeypatch() -> Generator["MonkeyPatch", None, None]: - """The returned ``monkeypatch`` fixture provides these - helper methods to modify objects, dictionaries or os.environ:: + """A convenient fixture for monkey-patching. + + The fixture provides these methods to modify objects, dictionaries or + os.environ:: monkeypatch.setattr(obj, name, value, raising=True) monkeypatch.delattr(obj, name, raising=True) @@ -39,10 +41,9 @@ def monkeypatch() -> Generator["MonkeyPatch", None, None]: monkeypatch.syspath_prepend(path) monkeypatch.chdir(path) - All modifications will be undone after the requesting - test function or fixture has finished. The ``raising`` - parameter determines if a KeyError or AttributeError - will be raised if the set/deletion operation has no target. + All modifications will be undone after the requesting test function or + fixture has finished. The ``raising`` parameter determines if a KeyError + or AttributeError will be raised if the set/deletion operation has no target. """ mpatch = MonkeyPatch() yield mpatch @@ -50,7 +51,7 @@ def monkeypatch() -> Generator["MonkeyPatch", None, None]: def resolve(name: str) -> object: - # simplified from zope.dottedname + # Simplified from zope.dottedname. parts = name.split(".") used = parts.pop(0) @@ -63,12 +64,11 @@ def resolve(name: str) -> object: pass else: continue - # we use explicit un-nesting of the handling block in order - # to avoid nested exceptions on python 3 + # We use explicit un-nesting of the handling block in order + # to avoid nested exceptions. try: __import__(used) except ImportError as ex: - # str is used for py2 vs py3 expected = str(ex).split()[-1] if expected == used: raise @@ -111,8 +111,8 @@ def __repr__(self) -> str: class MonkeyPatch: - """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes. - """ + """Object returned by the ``monkeypatch`` fixture keeping a record of + setattr/item/env/syspath changes.""" def __init__(self) -> None: self._setattr = [] # type: List[Tuple[object, str, object]] @@ -124,9 +124,10 @@ def __init__(self) -> None: @contextmanager def context(self) -> Generator["MonkeyPatch", None, None]: - """ - Context manager that returns a new :class:`MonkeyPatch` object which - undoes any patching done inside the ``with`` block upon exit: + """Context manager that returns a new :class:`MonkeyPatch` object + which undoes any patching done inside the ``with`` block upon exit. + + Example: .. code-block:: python @@ -166,18 +167,16 @@ def setattr( # noqa: F811 value: object = notset, raising: bool = True, ) -> None: - """ Set attribute value on target, memorizing the old value. - By default raise AttributeError if the attribute did not exist. + """Set attribute value on target, memorizing the old value. For convenience you can specify a string as ``target`` which will be interpreted as a dotted import path, with the last part - being the attribute name. Example: + being the attribute name. For example, ``monkeypatch.setattr("os.getcwd", lambda: "/")`` would set the ``getcwd`` function of the ``os`` module. - The ``raising`` value determines if the setattr should fail - if the attribute is not already present (defaults to True - which means it will raise). + Raises AttributeError if the attribute does not exist, unless + ``raising`` is set to False. """ __tracebackhide__ = True import inspect @@ -215,15 +214,14 @@ def delattr( name: Union[str, Notset] = notset, raising: bool = True, ) -> None: - """ Delete attribute ``name`` from ``target``, by default raise - AttributeError it the attribute did not previously exist. + """Delete attribute ``name`` from ``target``. If no ``name`` is specified and ``target`` is a string it will be interpreted as a dotted import path with the last part being the attribute name. - If ``raising`` is set to False, no exception will be raised if the - attribute is missing. + Raises AttributeError it the attribute does not exist, unless + ``raising`` is set to False. """ __tracebackhide__ = True import inspect @@ -249,15 +247,15 @@ def delattr( delattr(target, name) def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None: - """ Set dictionary entry ``name`` to value. """ + """Set dictionary entry ``name`` to value.""" self._setitem.append((dic, name, dic.get(name, notset))) dic[name] = value def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None: - """ Delete ``name`` from dict. Raise KeyError if it doesn't exist. + """Delete ``name`` from dict. - If ``raising`` is set to False, no exception will be raised if the - key is missing. + Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to + False. """ if name not in dic: if raising: @@ -267,9 +265,12 @@ def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> N del dic[name] def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: - """ Set environment variable ``name`` to ``value``. If ``prepend`` - is a character, read the current environment variable value - and prepend the ``value`` adjoined with the ``prepend`` character.""" + """Set environment variable ``name`` to ``value``. + + If ``prepend`` is a character, read the current environment variable + value and prepend the ``value`` adjoined with the ``prepend`` + character. + """ if not isinstance(value, str): warnings.warn( pytest.PytestWarning( @@ -286,17 +287,16 @@ def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: self.setitem(os.environ, name, value) def delenv(self, name: str, raising: bool = True) -> None: - """ Delete ``name`` from the environment. Raise KeyError if it does - not exist. + """Delete ``name`` from the environment. - If ``raising`` is set to False, no exception will be raised if the - environment variable is missing. + Raises ``KeyError`` if it does not exist, unless ``raising`` is set to + False. """ environ = os.environ # type: MutableMapping[str, str] self.delitem(environ, name, raising=raising) def syspath_prepend(self, path) -> None: - """ Prepend ``path`` to ``sys.path`` list of import locations. """ + """Prepend ``path`` to ``sys.path`` list of import locations.""" from pkg_resources import fixup_namespace_packages if self._savesyspath is None: @@ -318,7 +318,8 @@ def syspath_prepend(self, path) -> None: invalidate_caches() def chdir(self, path) -> None: - """ Change the current working directory to the specified path. + """Change the current working directory to the specified path. + Path can be a string or a py.path.local object. """ if self._cwd is None: @@ -326,15 +327,16 @@ def chdir(self, path) -> None: if hasattr(path, "chdir"): path.chdir() elif isinstance(path, Path): - # modern python uses the fspath protocol here LEGACY + # Modern python uses the fspath protocol here LEGACY os.chdir(str(path)) else: os.chdir(path) def undo(self) -> None: - """ Undo previous changes. This call consumes the - undo stack. Calling it a second time has no effect unless - you do more monkeypatching after the undo call. + """Undo previous changes. + + This call consumes the undo stack. Calling it a second time has no + effect unless you do more monkeypatching after the undo call. There is generally no need to call `undo()`, since it is called automatically during tear-down. @@ -356,7 +358,7 @@ def undo(self) -> None: try: del dictionary[key] except KeyError: - pass # was already deleted, so we have the desired state + pass # Was already deleted, so we have the desired state. else: dictionary[key] = value self._setitem[:] = [] diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index d53d591e742..cc1cc7ebdd0 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -66,19 +66,23 @@ def _splitnode(nodeid: str) -> Tuple[str, ...]: ['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo'] """ if nodeid == "": - # If there is no root node at all, return an empty list so the caller's logic can remain sane + # If there is no root node at all, return an empty list so the caller's + # logic can remain sane. return () parts = nodeid.split(SEP) - # Replace single last element 'test_foo.py::Bar' with multiple elements 'test_foo.py', 'Bar' + # Replace single last element 'test_foo.py::Bar' with multiple elements + # 'test_foo.py', 'Bar'. parts[-1:] = parts[-1].split("::") - # Convert parts into a tuple to avoid possible errors with caching of a mutable type + # Convert parts into a tuple to avoid possible errors with caching of a + # mutable type. return tuple(parts) def ischildnode(baseid: str, nodeid: str) -> bool: """Return True if the nodeid is a child node of the baseid. - E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp' + E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', + but not of 'foo/blorp'. """ base_parts = _splitnode(baseid) node_parts = _splitnode(nodeid) @@ -100,8 +104,11 @@ def _create(self, *k, **kw): class Node(metaclass=NodeMeta): - """ base class for Collector and Item the test collection tree. - Collector subclasses have children, Items are terminal nodes.""" + """Base class for Collector and Item, the components of the test + collection tree. + + Collector subclasses have children; Items are leaf nodes. + """ # Use __slots__ to make attribute access faster. # Note that __dict__ is still available. @@ -125,13 +132,13 @@ def __init__( fspath: Optional[py.path.local] = None, nodeid: Optional[str] = None, ) -> None: - #: a unique name within the scope of the parent node + #: A unique name within the scope of the parent node. self.name = name - #: the parent collector node. + #: The parent collector node. self.parent = parent - #: the pytest config object + #: The pytest config object. if config: self.config = config # type: Config else: @@ -139,7 +146,7 @@ def __init__( raise TypeError("config or parent must be provided") self.config = parent.config - #: the session this node is part of + #: The pytest session this node is part of. if session: self.session = session else: @@ -147,19 +154,19 @@ def __init__( raise TypeError("session or parent must be provided") self.session = parent.session - #: filesystem path where this node was collected from (can be None) + #: Filesystem path where this node was collected from (can be None). self.fspath = fspath or getattr(parent, "fspath", None) - #: keywords/markers collected from all scopes + #: Keywords/markers collected from all scopes. self.keywords = NodeKeywords(self) - #: the marker objects belonging to this node + #: The marker objects belonging to this node. self.own_markers = [] # type: List[Mark] - #: allow adding of extra keywords to use for matching + #: Allow adding of extra keywords to use for matching. self.extra_keyword_matches = set() # type: Set[str] - # used for storing artificial fixturedefs for direct parametrization + # Used for storing artificial fixturedefs for direct parametrization. self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef] if nodeid is not None: @@ -178,15 +185,15 @@ def __init__( @classmethod def from_parent(cls, parent: "Node", **kw): - """ - Public Constructor for Nodes + """Public constructor for Nodes. This indirection got introduced in order to enable removing the fragile logic from the node constructors. - Subclasses can use ``super().from_parent(...)`` when overriding the construction + Subclasses can use ``super().from_parent(...)`` when overriding the + construction. - :param parent: the parent node of this test Node + :param parent: The parent node of this Node. """ if "config" in kw: raise TypeError("config is not a valid argument for from_parent") @@ -196,27 +203,27 @@ def from_parent(cls, parent: "Node", **kw): @property def ihook(self): - """ fspath sensitive hook proxy used to call pytest hooks""" + """fspath-sensitive hook proxy used to call pytest hooks.""" return self.session.gethookproxy(self.fspath) def __repr__(self) -> str: return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) def warn(self, warning: "PytestWarning") -> None: - """Issue a warning for this item. + """Issue a warning for this Node. - Warnings will be displayed after the test session, unless explicitly suppressed + Warnings will be displayed after the test session, unless explicitly suppressed. - :param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning. + :param Warning warning: + The warning instance to issue. Must be a subclass of PytestWarning. - :raise ValueError: if ``warning`` instance is not a subclass of PytestWarning. + :raises ValueError: If ``warning`` instance is not a subclass of PytestWarning. Example usage: .. code-block:: python node.warn(PytestWarning("some message")) - """ from _pytest.warning_types import PytestWarning @@ -232,10 +239,11 @@ def warn(self, warning: "PytestWarning") -> None: warning, category=None, filename=str(path), lineno=lineno + 1, ) - # methods for ordering nodes + # Methods for ordering nodes. + @property def nodeid(self) -> str: - """ a ::-separated string denoting its collection tree address. """ + """A ::-separated string denoting its collection tree address.""" return self._nodeid def __hash__(self) -> int: @@ -248,8 +256,8 @@ def teardown(self) -> None: pass def listchain(self) -> List["Node"]: - """ return list of all parent collectors up to self, - starting from root of collection tree. """ + """Return list of all parent collectors up to self, starting from + the root of collection tree.""" chain = [] item = self # type: Optional[Node] while item is not None: @@ -261,12 +269,10 @@ def listchain(self) -> List["Node"]: def add_marker( self, marker: Union[str, MarkDecorator], append: bool = True ) -> None: - """dynamically add a marker object to the node. + """Dynamically add a marker object to the node. - :type marker: ``str`` or ``pytest.mark.*`` object - :param marker: - ``append=True`` whether to append the marker, - if ``False`` insert at position ``0``. + :param append: + Whether to append the marker, or prepend it. """ from _pytest.mark import MARK_GEN @@ -283,21 +289,19 @@ def add_marker( self.own_markers.insert(0, marker_.mark) def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]: - """ - :param name: if given, filter the results by the name attribute + """Iterate over all markers of the node. - iterate over all markers of the node + :param name: If given, filter the results by the name attribute. """ return (x[1] for x in self.iter_markers_with_node(name=name)) def iter_markers_with_node( self, name: Optional[str] = None ) -> Iterator[Tuple["Node", Mark]]: - """ - :param name: if given, filter the results by the name attribute + """Iterate over all markers of the node. - iterate over all markers of the node - returns sequence of tuples (node, mark) + :param name: If given, filter the results by the name attribute. + :returns: An iterator of (node, mark) tuples. """ for node in reversed(self.listchain()): for mark in node.own_markers: @@ -315,16 +319,16 @@ def get_closest_marker(self, name: str, default: Mark) -> Mark: # noqa: F811 def get_closest_marker( # noqa: F811 self, name: str, default: Optional[Mark] = None ) -> Optional[Mark]: - """return the first marker matching the name, from closest (for example function) to farther level (for example - module level). + """Return the first marker matching the name, from closest (for + example function) to farther level (for example module level). - :param default: fallback return value of no marker was found - :param name: name to filter by + :param default: Fallback return value if no marker was found. + :param name: Name to filter by. """ return next(self.iter_markers(name=name), default) def listextrakeywords(self) -> Set[str]: - """ Return a set of all extra keywords in self and any parents.""" + """Return a set of all extra keywords in self and any parents.""" extra_keywords = set() # type: Set[str] for item in self.listchain(): extra_keywords.update(item.extra_keyword_matches) @@ -334,7 +338,7 @@ def listnames(self) -> List[str]: return [x.name for x in self.listchain()] def addfinalizer(self, fin: Callable[[], object]) -> None: - """ register a function to be called when this node is finalized. + """Register a function to be called when this node is finalized. This method can only be called when this node is active in a setup chain, for example during self.setup(). @@ -342,8 +346,8 @@ def addfinalizer(self, fin: Callable[[], object]) -> None: self.session._setupstate.addfinalizer(fin, self) def getparent(self, cls: "Type[_NodeType]") -> Optional[_NodeType]: - """ get the next parent node (including ourself) - which is an instance of the given class""" + """Get the next parent node (including self) which is an instance of + the given class.""" current = self # type: Optional[Node] while current and not isinstance(current, cls): current = current.parent @@ -411,8 +415,7 @@ def repr_failure( excinfo: ExceptionInfo[BaseException], style: "Optional[_TracebackStyle]" = None, ) -> Union[str, TerminalRepr]: - """ - Return a representation of a collection or test failure. + """Return a representation of a collection or test failure. :param excinfo: Exception information for the failure. """ @@ -422,13 +425,13 @@ def repr_failure( def get_fslocation_from_item( node: "Node", ) -> Tuple[Union[str, py.path.local], Optional[int]]: - """Tries to extract the actual location from a node, depending on available attributes: + """Try to extract the actual location from a node, depending on available attributes: * "location": a pair (path, lineno) * "obj": a Python object that the node wraps. * "fspath": just a path - :rtype: a tuple of (str|LocalPath, int) with filename and line number. + :rtype: A tuple of (str|py.path.local, int) with filename and line number. """ # See Item.location. location = getattr( @@ -443,25 +446,22 @@ def get_fslocation_from_item( class Collector(Node): - """ Collector instances create children through collect() - and thus iteratively build a tree. - """ + """Collector instances create children through collect() and thus + iteratively build a tree.""" class CollectError(Exception): - """ an error during collection, contains a custom message. """ + """An error during collection, contains a custom message.""" def collect(self) -> Iterable[Union["Item", "Collector"]]: - """ returns a list of children (items and collectors) - for this collection node. - """ + """Return a list of children (items and collectors) for this + collection node.""" raise NotImplementedError("abstract") # TODO: This omits the style= parameter which breaks Liskov Substitution. def repr_failure( # type: ignore[override] self, excinfo: ExceptionInfo[BaseException] ) -> Union[str, TerminalRepr]: - """ - Return a representation of a collection failure. + """Return a representation of a collection failure. :param excinfo: Exception information for the failure. """ @@ -538,24 +538,22 @@ def __init__( @classmethod def from_parent(cls, parent, *, fspath, **kw): - """ - The public constructor - """ + """The public constructor.""" return super().from_parent(parent=parent, fspath=fspath, **kw) def _gethookproxy(self, fspath: py.path.local): - # check if we have the common case of running - # hooks with all conftest.py files + # Check if we have the common case of running + # hooks with all conftest.py files. pm = self.config.pluginmanager my_conftestmodules = pm._getconftestmodules( fspath, self.config.getoption("importmode") ) remove_mods = pm._conftest_plugins.difference(my_conftestmodules) if remove_mods: - # one or more conftests are not in use at this fspath + # One or more conftests are not in use at this fspath. proxy = FSHookProxy(pm, remove_mods) else: - # all plugins are active for this fspath + # All plugins are active for this fspath. proxy = self.config.hook return proxy @@ -605,12 +603,13 @@ def _collectfile( class File(FSCollector): - """ base class for collecting tests from a file. """ + """Base class for collecting tests from a file.""" class Item(Node): - """ a basic test invocation item. Note that for a single function - there might be multiple test invocation items. + """A basic test invocation item. + + Note that for a single function there might be multiple test invocation items. """ nextitem = None @@ -626,17 +625,16 @@ def __init__( super().__init__(name, parent, config, session, nodeid=nodeid) self._report_sections = [] # type: List[Tuple[str, str, str]] - #: user properties is a list of tuples (name, value) that holds user - #: defined properties for this test. + #: A list of tuples (name, value) that holds user defined properties + #: for this test. self.user_properties = [] # type: List[Tuple[str, object]] def runtest(self) -> None: raise NotImplementedError("runtest must be implemented by Item subclass") def add_report_section(self, when: str, key: str, content: str) -> None: - """ - Adds a new report section, similar to what's done internally to add stdout and - stderr captured output:: + """Add a new report section, similar to what's done internally to add + stdout and stderr captured output:: item.add_report_section("call", "stdout", "report section contents") @@ -645,7 +643,6 @@ def add_report_section(self, when: str, key: str, content: str) -> None: :param str key: Name of the section, can be customized at will. Pytest uses ``"stdout"`` and ``"stderr"`` internally. - :param str content: The full contents as a string. """ diff --git a/src/_pytest/nose.py b/src/_pytest/nose.py index 8bdc310ac18..bb8f99772ac 100644 --- a/src/_pytest/nose.py +++ b/src/_pytest/nose.py @@ -1,4 +1,4 @@ -""" run test suites written for nose. """ +"""Run testsuites written for nose.""" from _pytest import python from _pytest import unittest from _pytest.config import hookimpl @@ -9,9 +9,9 @@ def pytest_runtest_setup(item): if is_potential_nosetest(item): if not call_optional(item.obj, "setup"): - # call module level setup if there is no object level one + # Call module level setup if there is no object level one. call_optional(item.parent.obj, "setup") - # XXX this implies we only call teardown when setup worked + # XXX This implies we only call teardown when setup worked. item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item) @@ -22,8 +22,8 @@ def teardown_nose(item): def is_potential_nosetest(item: Item) -> bool: - # extra check needed since we do not do nose style setup/teardown - # on direct unittest style classes + # Extra check needed since we do not do nose style setup/teardown + # on direct unittest style classes. return isinstance(item, python.Function) and not isinstance( item, unittest.TestCaseFunction ) @@ -34,6 +34,6 @@ def call_optional(obj, name): isfixture = hasattr(method, "_pytestfixturefunction") if method is not None and not isfixture and callable(method): # If there's any problems allow the exception to raise rather than - # silently ignoring them + # silently ignoring them. method() return True diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 751cf9474fb..f083689edba 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -1,7 +1,5 @@ -""" -exception classes and constants handling test outcomes -as well as functions creating them -""" +"""Exception classes and constants handling test outcomes as well as +functions creating them.""" import sys from typing import Any from typing import Callable @@ -9,7 +7,7 @@ from typing import Optional from typing import TypeVar -TYPE_CHECKING = False # avoid circular import through compat +TYPE_CHECKING = False # Avoid circular import through compat. if TYPE_CHECKING: from typing import NoReturn @@ -25,9 +23,8 @@ class OutcomeException(BaseException): - """ OutcomeException and its subclass instances indicate and - contain info about test and collection outcomes. - """ + """OutcomeException and its subclass instances indicate and contain info + about test and collection outcomes.""" def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None: if msg is not None and not isinstance(msg, str): @@ -67,13 +64,13 @@ def __init__( class Failed(OutcomeException): - """ raised from an explicit call to pytest.fail() """ + """Raised from an explicit call to pytest.fail().""" __module__ = "builtins" class Exit(Exception): - """ raised for immediate program exits (no tracebacks/summaries)""" + """Raised for immediate program exits (no tracebacks/summaries).""" def __init__( self, msg: str = "unknown reason", returncode: Optional[int] = None @@ -104,16 +101,15 @@ def decorate(func: _F) -> _WithException[_F, _ET]: return decorate -# exposed helper methods +# Exposed helper methods. @_with_exception(Exit) def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn": - """ - Exit testing process. + """Exit testing process. - :param str msg: message to display upon exit. - :param int returncode: return code to be used when exiting pytest. + :param str msg: Message to display upon exit. + :param int returncode: Return code to be used when exiting pytest. """ __tracebackhide__ = True raise Exit(msg, returncode) @@ -121,20 +117,20 @@ def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn": @_with_exception(Skipped) def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn": - """ - Skip an executing test with the given message. + """Skip an executing test with the given message. This function should be called only during testing (setup, call or teardown) or during collection by using the ``allow_module_level`` flag. This function can be called in doctests as well. - :kwarg bool allow_module_level: allows this function to be called at - module level, skipping the rest of the module. Default to False. + :param bool allow_module_level: + Allows this function to be called at module level, skipping the rest + of the module. Defaults to False. .. note:: - It is better to use the :ref:`pytest.mark.skipif ref` marker when possible to declare a test to be - skipped under certain conditions like mismatching platforms or - dependencies. + It is better to use the :ref:`pytest.mark.skipif ref` marker when + possible to declare a test to be skipped under certain conditions + like mismatching platforms or dependencies. Similarly, use the ``# doctest: +SKIP`` directive (see `doctest.SKIP `_) to skip a doctest statically. @@ -145,11 +141,12 @@ def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn": @_with_exception(Failed) def fail(msg: str = "", pytrace: bool = True) -> "NoReturn": - """ - Explicitly fail an executing test with the given message. + """Explicitly fail an executing test with the given message. - :param str msg: the message to show the user as reason for the failure. - :param bool pytrace: if false the msg represents the full failure information and no + :param str msg: + The message to show the user as reason for the failure. + :param bool pytrace: + If False, msg represents the full failure information and no python traceback will be reported. """ __tracebackhide__ = True @@ -157,19 +154,19 @@ def fail(msg: str = "", pytrace: bool = True) -> "NoReturn": class XFailed(Failed): - """ raised from an explicit call to pytest.xfail() """ + """Raised from an explicit call to pytest.xfail().""" @_with_exception(XFailed) def xfail(reason: str = "") -> "NoReturn": - """ - Imperatively xfail an executing test or setup functions with the given reason. + """Imperatively xfail an executing test or setup function with the given reason. This function should be called only during testing (setup, call or teardown). .. note:: - It is better to use the :ref:`pytest.mark.xfail ref` marker when possible to declare a test to be - xfailed under certain conditions like known bugs or missing features. + It is better to use the :ref:`pytest.mark.xfail ref` marker when + possible to declare a test to be xfailed under certain conditions + like known bugs or missing features. """ __tracebackhide__ = True raise XFailed(reason) @@ -178,17 +175,20 @@ def xfail(reason: str = "") -> "NoReturn": def importorskip( modname: str, minversion: Optional[str] = None, reason: Optional[str] = None ) -> Any: - """Imports and returns the requested module ``modname``, or skip the + """Import and return the requested module ``modname``, or skip the current test if the module cannot be imported. - :param str modname: the name of the module to import - :param str minversion: if given, the imported module's ``__version__`` - attribute must be at least this minimal version, otherwise the test is - still skipped. - :param str reason: if given, this reason is shown as the message when the - module cannot be imported. - :returns: The imported module. This should be assigned to its canonical - name. + :param str modname: + The name of the module to import. + :param str minversion: + If given, the imported module's ``__version__`` attribute must be at + least this minimal version, otherwise the test is still skipped. + :param str reason: + If given, this reason is shown as the message when the module cannot + be imported. + + :returns: + The imported module. This should be assigned to its canonical name. Example:: @@ -200,9 +200,9 @@ def importorskip( compile(modname, "", "eval") # to catch syntaxerrors with warnings.catch_warnings(): - # make sure to ignore ImportWarnings that might happen because + # Make sure to ignore ImportWarnings that might happen because # of existing directories with the same name we're trying to - # import but without a __init__.py file + # import but without a __init__.py file. warnings.simplefilter("ignore") try: __import__(modname) diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index a3432c7a10c..0546d237762 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -1,4 +1,4 @@ -""" submit failure or test session information to a pastebin service. """ +"""Submit failure or test session information to a pastebin service.""" import tempfile from io import StringIO from typing import IO @@ -32,11 +32,11 @@ def pytest_addoption(parser: Parser) -> None: def pytest_configure(config: Config) -> None: if config.option.pastebin == "all": tr = config.pluginmanager.getplugin("terminalreporter") - # if no terminal reporter plugin is present, nothing we can do here; + # If no terminal reporter plugin is present, nothing we can do here; # this can happen when this function executes in a worker node - # when using pytest-xdist, for example + # when using pytest-xdist, for example. if tr is not None: - # pastebin file will be utf-8 encoded binary file + # pastebin file will be UTF-8 encoded binary file. config._store[pastebinfile_key] = tempfile.TemporaryFile("w+b") oldwrite = tr._tw.write @@ -52,26 +52,25 @@ def tee_write(s, **kwargs): def pytest_unconfigure(config: Config) -> None: if pastebinfile_key in config._store: pastebinfile = config._store[pastebinfile_key] - # get terminal contents and delete file + # Get terminal contents and delete file. pastebinfile.seek(0) sessionlog = pastebinfile.read() pastebinfile.close() del config._store[pastebinfile_key] - # undo our patching in the terminal reporter + # Undo our patching in the terminal reporter. tr = config.pluginmanager.getplugin("terminalreporter") del tr._tw.__dict__["write"] - # write summary + # Write summary. tr.write_sep("=", "Sending information to Paste Service") pastebinurl = create_new_paste(sessionlog) tr.write_line("pastebin session-log: %s\n" % pastebinurl) def create_new_paste(contents: Union[str, bytes]) -> str: - """ - Creates a new paste using bpaste.net service. + """Create a new paste using the bpaste.net service. - :contents: paste contents string - :returns: url to the pasted contents or error message + :contents: Paste contents string. + :returns: URL to the pasted contents, or an error message. """ import re from urllib.request import urlopen diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index ba7e9948a59..ea263be7009 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -49,23 +49,21 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath: def ensure_reset_dir(path: Path) -> None: - """ - ensures the given path is an empty directory - """ + """Ensure the given path is an empty directory.""" if path.exists(): rm_rf(path) path.mkdir() def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool: - """Handles known read-only errors during rmtree. + """Handle known read-only errors during rmtree. The returned value is used only by our own tests. """ exctype, excvalue = exc[:2] - # another process removed the file in the middle of the "rm_rf" (xdist for example) - # more context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018 + # Another process removed the file in the middle of the "rm_rf" (xdist for example). + # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018 if isinstance(excvalue, FileNotFoundError): return False @@ -101,7 +99,7 @@ def chmod_rw(p: str) -> None: if p.is_file(): for parent in p.parents: chmod_rw(str(parent)) - # stop when we reach the original path passed to rm_rf + # Stop when we reach the original path passed to rm_rf. if parent == start_path: break chmod_rw(str(path)) @@ -129,7 +127,7 @@ def ensure_extended_length_path(path: Path) -> Path: def get_extended_length_path_str(path: str) -> str: - """Converts to extended length path as a str""" + """Convert a path to a Windows extended length path.""" long_path_prefix = "\\\\?\\" unc_long_path_prefix = "\\\\?\\UNC\\" if path.startswith((long_path_prefix, unc_long_path_prefix)): @@ -142,15 +140,14 @@ def get_extended_length_path_str(path: str) -> str: def rm_rf(path: Path) -> None: """Remove the path contents recursively, even if some elements - are read-only. - """ + are read-only.""" path = ensure_extended_length_path(path) onerror = partial(on_rm_rf_error, start_path=path) shutil.rmtree(str(path), onerror=onerror) def find_prefixed(root: Path, prefix: str) -> Iterator[Path]: - """finds all elements in root that begin with the prefix, case insensitive""" + """Find all elements in root that begin with the prefix, case insensitive.""" l_prefix = prefix.lower() for x in root.iterdir(): if x.name.lower().startswith(l_prefix): @@ -158,10 +155,10 @@ def find_prefixed(root: Path, prefix: str) -> Iterator[Path]: def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]: - """ - :param iter: iterator over path names - :param prefix: expected prefix of the path names - :returns: the parts of the paths following the prefix + """Return the parts of the paths following the prefix. + + :param iter: Iterator over path names. + :param prefix: Expected prefix of the path names. """ p_len = len(prefix) for p in iter: @@ -169,13 +166,12 @@ def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]: def find_suffixes(root: Path, prefix: str) -> Iterator[str]: - """combines find_prefixes and extract_suffixes - """ + """Combine find_prefixes and extract_suffixes.""" return extract_suffixes(find_prefixed(root, prefix), prefix) def parse_num(maybe_num) -> int: - """parses number path suffixes, returns -1 on error""" + """Parse number path suffixes, returns -1 on error.""" try: return int(maybe_num) except ValueError: @@ -185,13 +181,13 @@ def parse_num(maybe_num) -> int: def _force_symlink( root: Path, target: Union[str, PurePath], link_to: Union[str, Path] ) -> None: - """helper to create the current symlink + """Helper to create the current symlink. - it's full of race conditions that are reasonably ok to ignore - for the context of best effort linking to the latest test run + It's full of race conditions that are reasonably OK to ignore + for the context of best effort linking to the latest test run. - the presumption being that in case of much parallelism - the inaccuracy is going to be acceptable + The presumption being that in case of much parallelism + the inaccuracy is going to be acceptable. """ current_symlink = root.joinpath(target) try: @@ -205,7 +201,7 @@ def _force_symlink( def make_numbered_dir(root: Path, prefix: str) -> Path: - """create a directory with an increased number as suffix for the given prefix""" + """Create a directory with an increased number as suffix for the given prefix.""" for i in range(10): # try up to 10 times to create the folder max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) @@ -226,7 +222,7 @@ def make_numbered_dir(root: Path, prefix: str) -> Path: def create_cleanup_lock(p: Path) -> Path: - """crates a lock to prevent premature folder cleanup""" + """Create a lock to prevent premature folder cleanup.""" lock_path = get_lock_path(p) try: fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) @@ -243,7 +239,7 @@ def create_cleanup_lock(p: Path) -> Path: def register_cleanup_lock_removal(lock_path: Path, register=atexit.register): - """registers a cleanup function for removing a lock, by default on atexit""" + """Register a cleanup function for removing a lock, by default on atexit.""" pid = os.getpid() def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None: @@ -260,7 +256,8 @@ def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> Non def maybe_delete_a_numbered_dir(path: Path) -> None: - """removes a numbered directory if its lock can be obtained and it does not seem to be in use""" + """Remove a numbered directory if its lock can be obtained and it does + not seem to be in use.""" path = ensure_extended_length_path(path) lock_path = None try: @@ -277,8 +274,8 @@ def maybe_delete_a_numbered_dir(path: Path) -> None: # * process cwd (Windows) return finally: - # if we created the lock, ensure we remove it even if we failed - # to properly remove the numbered dir + # If we created the lock, ensure we remove it even if we failed + # to properly remove the numbered dir. if lock_path is not None: try: lock_path.unlink() @@ -287,7 +284,7 @@ def maybe_delete_a_numbered_dir(path: Path) -> None: def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool: - """checks if `path` is deletable based on whether the lock file is expired""" + """Check if `path` is deletable based on whether the lock file is expired.""" if path.is_symlink(): return False lock = get_lock_path(path) @@ -304,9 +301,9 @@ def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> return False else: if lock_time < consider_lock_dead_if_created_before: - # wa want to ignore any errors while trying to remove the lock such as: - # - PermissionDenied, like the file permissions have changed since the lock creation - # - FileNotFoundError, in case another pytest process got here first. + # We want to ignore any errors while trying to remove the lock such as: + # - PermissionDenied, like the file permissions have changed since the lock creation; + # - FileNotFoundError, in case another pytest process got here first; # and any other cause of failure. with contextlib.suppress(OSError): lock.unlink() @@ -315,13 +312,13 @@ def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None: - """tries to cleanup a folder if we can ensure it's deletable""" + """Try to cleanup a folder if we can ensure it's deletable.""" if ensure_deletable(path, consider_lock_dead_if_created_before): maybe_delete_a_numbered_dir(path) def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]: - """lists candidates for numbered directories to be removed - follows py.path""" + """List candidates for numbered directories to be removed - follows py.path.""" max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) max_delete = max_existing - keep paths = find_prefixed(root, prefix) @@ -335,7 +332,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]: def cleanup_numbered_dir( root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float ) -> None: - """cleanup for lock driven numbered directories""" + """Cleanup for lock driven numbered directories.""" for path in cleanup_candidates(root, prefix, keep): try_cleanup(path, consider_lock_dead_if_created_before) for path in root.glob("garbage-*"): @@ -345,7 +342,7 @@ def cleanup_numbered_dir( def make_numbered_dir_with_cleanup( root: Path, prefix: str, keep: int, lock_timeout: float ) -> Path: - """creates a numbered dir with a cleanup lock and removes old ones""" + """Create a numbered dir with a cleanup lock and remove old ones.""" e = None for i in range(10): try: @@ -381,17 +378,18 @@ def resolve_from_str(input: str, root: py.path.local) -> Path: def fnmatch_ex(pattern: str, path) -> bool: - """FNMatcher port from py.path.common which works with PurePath() instances. + """A port of FNMatcher from py.path.common which works with PurePath() instances. - The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions - for each part of the path, while this algorithm uses the whole path instead. + The difference between this algorithm and PurePath.match() is that the + latter matches "**" glob expressions for each part of the path, while + this algorithm uses the whole path instead. For example: - "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" with this algorithm, but not with - PurePath.match(). + "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" + with this algorithm, but not with PurePath.match(). - This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according - this logic. + This algorithm was ported to keep backward-compatibility with existing + settings which assume paths match according this logic. References: * https://bugs.python.org/issue29249 @@ -421,7 +419,7 @@ def parts(s: str) -> Set[str]: def symlink_or_skip(src, dst, **kwargs): - """Makes a symlink or skips the test in case symlinks are not supported.""" + """Make a symlink, or skip the test in case symlinks are not supported.""" try: os.symlink(str(src), str(dst), **kwargs) except OSError as e: @@ -429,7 +427,7 @@ def symlink_or_skip(src, dst, **kwargs): class ImportMode(Enum): - """Possible values for `mode` parameter of `import_path`""" + """Possible values for `mode` parameter of `import_path`.""" prepend = "prepend" append = "append" @@ -450,8 +448,7 @@ def import_path( *, mode: Union[str, ImportMode] = ImportMode.prepend ) -> ModuleType: - """ - Imports and returns a module from the given path, which can be a file (a module) or + """Import and return a module from the given path, which can be a file (a module) or a directory (a package). The import mechanism used is controlled by the `mode` parameter: @@ -467,7 +464,8 @@ def import_path( to import the module, which avoids having to use `__import__` and muck with `sys.path` at all. It effectively allows having same-named test modules in different places. - :raise ImportPathMismatchError: if after importing the given `path` and the module `__file__` + :raises ImportPathMismatchError: + If after importing the given `path` and the module `__file__` are different. Only raised in `prepend` and `append` modes. """ mode = ImportMode(mode) @@ -506,7 +504,7 @@ def import_path( pkg_root = path.parent module_name = path.stem - # change sys.path permanently: restoring it at the end of this function would cause surprising + # Change sys.path permanently: restoring it at the end of this function would cause surprising # problems because of delayed imports: for example, a conftest.py file imported by this function # might have local imports, which would fail at runtime if we restored sys.path. if mode is ImportMode.append: @@ -546,7 +544,8 @@ def import_path( def resolve_package_path(path: Path) -> Optional[Path]: """Return the Python package path by looking for the last directory upwards which still contains an __init__.py. - Return None if it can not be determined. + + Returns None if it can not be determined. """ result = None for parent in itertools.chain((path,), path.parents): diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 594abee9094..e0e7b1fbc4a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1,4 +1,4 @@ -"""(disabled by default) support for testing pytest and pytest plugins.""" +"""(Disabled by default) support for testing pytest and pytest plugins.""" import collections.abc import gc import importlib @@ -166,9 +166,7 @@ def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: def _pytest(request: FixtureRequest) -> "PytestArg": """Return a helper which offers a gethookrecorder(hook) method which returns a HookRecorder instance which helps to make assertions about called - hooks. - - """ + hooks.""" return PytestArg(request) @@ -208,7 +206,6 @@ class HookRecorder: This wraps all the hook calls in the plugin manager, recording each call before propagating the normal calls. - """ def __init__(self, pluginmanager: PytestPluginManager) -> None: @@ -285,7 +282,7 @@ def matchreport( ] = "pytest_runtest_logreport pytest_collectreport", when=None, ): - """return a testreport whose dotted import path matches""" + """Return a testreport whose dotted import path matches.""" values = [] for rep in self.getreports(names=names): if not when and rep.when != "call" and rep.passed: @@ -358,17 +355,14 @@ def clear(self) -> None: @pytest.fixture def linecomp() -> "LineComp": - """ - A :class: `LineComp` instance for checking that an input linearly - contains a sequence of strings. - """ + """A :class: `LineComp` instance for checking that an input linearly + contains a sequence of strings.""" return LineComp() @pytest.fixture(name="LineMatcher") def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]": - """ - A reference to the :class: `LineMatcher`. + """A reference to the :class: `LineMatcher`. This is instantiable with a list of lines (without their trailing newlines). This is useful for testing large texts, such as the output of commands. @@ -378,12 +372,10 @@ def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]": @pytest.fixture def testdir(request: FixtureRequest, tmpdir_factory: TempdirFactory) -> "Testdir": - """ - A :class: `TestDir` instance, that can be used to run and test pytest itself. + """A :class: `TestDir` instance, that can be used to run and test pytest itself. It is particularly useful for testing plugins. It is similar to the `tmpdir` fixture but provides methods which aid in testing pytest itself. - """ return Testdir(request, tmpdir_factory) @@ -406,9 +398,9 @@ def _config_for_test() -> Generator[Config, None, None]: config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles. -# regex to match the session duration string in the summary: "74.34s" +# Regex to match the session duration string in the summary: "74.34s". rex_session_duration = re.compile(r"\d+\.\d\ds") -# regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped" +# Regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped". rex_outcome = re.compile(r"(\d+) (\w+)") @@ -424,13 +416,13 @@ def __init__( ) -> None: try: self.ret = pytest.ExitCode(ret) # type: Union[int, ExitCode] - """the return value""" + """The return value.""" except ValueError: self.ret = ret self.outlines = outlines - """list of lines captured from stdout""" + """List of lines captured from stdout.""" self.errlines = errlines - """list of lines captured from stderr""" + """List of lines captured from stderr.""" self.stdout = LineMatcher(outlines) """:class:`LineMatcher` of stdout. @@ -438,9 +430,9 @@ def __init__( :func:`stdout.fnmatch_lines() ` method. """ self.stderr = LineMatcher(errlines) - """:class:`LineMatcher` of stderr""" + """:class:`LineMatcher` of stderr.""" self.duration = duration - """duration in seconds""" + """Duration in seconds.""" def __repr__(self) -> str: return ( @@ -456,19 +448,19 @@ def parseoutcomes(self) -> Dict[str, int]: ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== - Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}`` + Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``. """ return self.parse_summary_nouns(self.outlines) @classmethod def parse_summary_nouns(cls, lines) -> Dict[str, int]: - """Extracts the nouns from a pytest terminal summary line. + """Extract the nouns from a pytest terminal summary line. It always returns the plural noun for consistency:: ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== - Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}`` + Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``. """ for line in reversed(lines): if rex_session_duration.search(line): @@ -494,8 +486,7 @@ def assert_outcomes( xfailed: int = 0, ) -> None: """Assert that the specified outcomes appear with the respective - numbers (0 means it didn't occur) in the text output from a test run. - """ + numbers (0 means it didn't occur) in the text output from a test run.""" __tracebackhide__ = True d = self.parseoutcomes() @@ -551,7 +542,7 @@ def restore(self) -> None: class Testdir: """Temporary test directory with tools to test/run pytest itself. - This is based on the ``tmpdir`` fixture but provides a number of methods + This is based on the :fixture:`tmpdir` fixture but provides a number of methods which aid with testing pytest itself. Unless :py:meth:`chdir` is used all methods will use :py:attr:`tmpdir` as their current working directory. @@ -559,11 +550,11 @@ class Testdir: :ivar tmpdir: The :py:class:`py.path.local` instance of the temporary directory. - :ivar plugins: A list of plugins to use with :py:meth:`parseconfig` and + :ivar plugins: + A list of plugins to use with :py:meth:`parseconfig` and :py:meth:`runpytest`. Initially this is an empty list but plugins can be added to the list. The type of items to add to the list depends on the method using them so refer to them for details. - """ __test__ = False @@ -618,7 +609,6 @@ def finalize(self) -> None: Some methods modify the global interpreter state and this tries to clean this up. It does not remove the temporary directory however so it can be looked at after the test run has finished. - """ self._sys_modules_snapshot.restore() self._sys_path_snapshot.restore() @@ -626,9 +616,9 @@ def finalize(self) -> None: self.monkeypatch.undo() def __take_sys_modules_snapshot(self) -> SysModulesSnapshot: - # some zope modules used by twisted-related tests keep internal state + # Some zope modules used by twisted-related tests keep internal state # and can't be deleted; we had some trouble in the past with - # `zope.interface` for example + # `zope.interface` for example. def preserve_module(name): return name.startswith("zope") @@ -644,7 +634,6 @@ def chdir(self) -> None: """Cd into the temporary directory. This is done automatically upon instantiation. - """ self.tmpdir.chdir() @@ -673,12 +662,15 @@ def to_text(s): def makefile(self, ext: str, *args: str, **kwargs): r"""Create new file(s) in the testdir. - :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`. - :param list[str] args: All args will be treated as strings and joined using newlines. - The result will be written as contents to the file. The name of the - file will be based on the test function requesting this fixture. - :param kwargs: Each keyword is the name of a file, while the value of it will - be written as contents of the file. + :param str ext: + The extension the file(s) should use, including the dot, e.g. `.py`. + :param args: + All args are treated as strings and joined using newlines. + The result is written as contents to the file. The name of the + file is based on the test function requesting this fixture. + :param kwargs: + Each keyword is the name of a file, while the value of it will + be written as contents of the file. Examples: @@ -713,6 +705,7 @@ def makepyprojecttoml(self, source): def makepyfile(self, *args, **kwargs): r"""Shortcut for .makefile() with a .py extension. + Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting existing files. @@ -721,17 +714,18 @@ def makepyfile(self, *args, **kwargs): .. code-block:: python def test_something(testdir): - # initial file is created test_something.py + # Initial file is created test_something.py. testdir.makepyfile("foobar") - # to create multiple files, pass kwargs accordingly + # To create multiple files, pass kwargs accordingly. testdir.makepyfile(custom="foobar") - # at this point, both 'test_something.py' & 'custom.py' exist in the test directory + # At this point, both 'test_something.py' & 'custom.py' exist in the test directory. """ return self._makefile(".py", args, kwargs) def maketxtfile(self, *args, **kwargs): r"""Shortcut for .makefile() with a .txt extension. + Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting existing files. @@ -740,11 +734,11 @@ def maketxtfile(self, *args, **kwargs): .. code-block:: python def test_something(testdir): - # initial file is created test_something.txt + # Initial file is created test_something.txt. testdir.maketxtfile("foobar") - # to create multiple files, pass kwargs accordingly + # To create multiple files, pass kwargs accordingly. testdir.maketxtfile(custom="foobar") - # at this point, both 'test_something.txt' & 'custom.txt' exist in the test directory + # At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory. """ return self._makefile(".txt", args, kwargs) @@ -765,11 +759,10 @@ def mkdir(self, name) -> py.path.local: return self.tmpdir.mkdir(name) def mkpydir(self, name) -> py.path.local: - """Create a new python package. + """Create a new Python package. This creates a (sub)directory with an empty ``__init__.py`` file so it - gets recognised as a python package. - + gets recognised as a Python package. """ p = self.mkdir(name) p.ensure("__init__.py") @@ -779,8 +772,7 @@ def copy_example(self, name=None) -> py.path.local: """Copy file from project's directory into the testdir. :param str name: The name of the file to copy. - :return: path to the copied directory (inside ``self.tmpdir``). - + :returns: Path to the copied directory (inside ``self.tmpdir``). """ import warnings from _pytest.warning_types import PYTESTER_COPY_EXAMPLE @@ -830,12 +822,11 @@ def copy_example(self, name=None) -> py.path.local: def getnode(self, config: Config, arg): """Return the collection node of a file. - :param config: :py:class:`_pytest.config.Config` instance, see - :py:meth:`parseconfig` and :py:meth:`parseconfigure` to create the - configuration - - :param arg: a :py:class:`py.path.local` instance of the file - + :param _pytest.config.Config config: + A pytest config. + See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it. + :param py.path.local arg: + Path to the file. """ session = Session.from_config(config) assert "::" not in str(arg) @@ -851,8 +842,7 @@ def getpathnode(self, path): This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to create the (configured) pytest Config instance. - :param path: a :py:class:`py.path.local` instance of the file - + :param py.path.local path: Path to the file. """ config = self.parseconfigure(path) session = Session.from_config(config) @@ -867,7 +857,6 @@ def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]: This recurses into the collection node and returns a list of all the test items contained within. - """ session = colitems[0].session result = [] # type: List[Item] @@ -882,7 +871,6 @@ def runitem(self, source): provide a ``.getrunner()`` method which should return a runner which can run the test protocol for a single item, e.g. :py:func:`_pytest.runner.runtestprotocol`. - """ # used from runner functional tests item = self.getitem(source) @@ -898,12 +886,11 @@ def inline_runsource(self, source, *cmdlineargs): ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance for the result. - :param source: the source code of the test module + :param source: The source code of the test module. - :param cmdlineargs: any extra command line arguments to use - - :return: :py:class:`HookRecorder` instance of the result + :param cmdlineargs: Any extra command line arguments to use. + :returns: :py:class:`HookRecorder` instance of the result. """ p = self.makepyfile(source) values = list(cmdlineargs) + [p] @@ -915,7 +902,6 @@ def inline_genitems(self, *args): Runs the :py:func:`pytest.main` function to run all of pytest inside the test process itself like :py:meth:`inline_run`, but returns a tuple of the collected items and a :py:class:`HookRecorder` instance. - """ rec = self.inline_run("--collect-only", *args) items = [x.item for x in rec.getcalls("pytest_itemcollected")] @@ -930,14 +916,15 @@ def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): from that run than can be done by matching stdout/stderr from :py:meth:`runpytest`. - :param args: command line arguments to pass to :py:func:`pytest.main` - - :kwarg plugins: extra plugin instances the ``pytest.main()`` instance should use. - - :kwarg no_reraise_ctrlc: typically we reraise keyboard interrupts from the child run. If + :param args: + Command line arguments to pass to :py:func:`pytest.main`. + :param plugins: + Extra plugin instances the ``pytest.main()`` instance should use. + :param no_reraise_ctrlc: + Typically we reraise keyboard interrupts from the child run. If True, the KeyboardInterrupt exception is captured. - :return: a :py:class:`HookRecorder` instance + :returns: A :py:class:`HookRecorder` instance. """ # (maybe a cpython bug?) the importlib cache sometimes isn't updated # properly between file creation and inline_run (especially if imports @@ -977,8 +964,8 @@ class reprec: # type: ignore reprec.ret = ret # type: ignore[attr-defined] - # typically we reraise keyboard interrupts from the child run - # because it's our user requesting interruption of the testing + # Typically we reraise keyboard interrupts from the child run + # because it's our user requesting interruption of the testing. if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc: calls = reprec.getcalls("pytest_keyboard_interrupt") if calls and calls[-1].excinfo.type == KeyboardInterrupt: @@ -990,8 +977,7 @@ class reprec: # type: ignore def runpytest_inprocess(self, *args, **kwargs) -> RunResult: """Return result of running pytest in-process, providing a similar - interface to what self.runpytest() provides. - """ + interface to what self.runpytest() provides.""" syspathinsert = kwargs.pop("syspathinsert", False) if syspathinsert: @@ -1032,9 +1018,7 @@ class reprec: # type: ignore def runpytest(self, *args, **kwargs) -> RunResult: """Run pytest inline or in a subprocess, depending on the command line - option "--runpytest" and return a :py:class:`RunResult`. - - """ + option "--runpytest" and return a :py:class:`RunResult`.""" args = self._ensure_basetemp(args) if self._method == "inprocess": return self.runpytest_inprocess(*args, **kwargs) @@ -1061,7 +1045,6 @@ def parseconfig(self, *args) -> Config: If :py:attr:`plugins` has been populated they should be plugin modules to be registered with the PluginManager. - """ args = self._ensure_basetemp(args) @@ -1077,7 +1060,7 @@ def parseconfig(self, *args) -> Config: def parseconfigure(self, *args) -> Config: """Return a new pytest configured Config instance. - This returns a new :py:class:`_pytest.config.Config` instance like + Returns a new :py:class:`_pytest.config.Config` instance like :py:meth:`parseconfig`, but also calls the pytest_configure hook. """ config = self.parseconfig(*args) @@ -1087,15 +1070,14 @@ def parseconfigure(self, *args) -> Config: def getitem(self, source, funcname: str = "test_func") -> Item: """Return the test item for a test function. - This writes the source to a python file and runs pytest's collection on + Writes the source to a python file and runs pytest's collection on the resulting module, returning the test item for the requested function name. - :param source: the module source - - :param funcname: the name of the test function for which to return a - test item - + :param source: + The module source. + :param funcname: + The name of the test function for which to return a test item. """ items = self.getitems(source) for item in items: @@ -1108,9 +1090,8 @@ def getitem(self, source, funcname: str = "test_func") -> Item: def getitems(self, source) -> List[Item]: """Return all test items collected from the module. - This writes the source to a python file and runs pytest's collection on + Writes the source to a Python file and runs pytest's collection on the resulting module, returning all test items contained within. - """ modcol = self.getmodulecol(source) return self.genitems([modcol]) @@ -1118,18 +1099,19 @@ def getitems(self, source) -> List[Item]: def getmodulecol(self, source, configargs=(), withinit: bool = False): """Return the module collection node for ``source``. - This writes ``source`` to a file using :py:meth:`makepyfile` and then + Writes ``source`` to a file using :py:meth:`makepyfile` and then runs the pytest collection on it, returning the collection node for the test module. - :param source: the source code of the module to collect + :param source: + The source code of the module to collect. - :param configargs: any extra arguments to pass to - :py:meth:`parseconfigure` - - :param withinit: whether to also write an ``__init__.py`` file to the - same directory to ensure it is a package + :param configargs: + Any extra arguments to pass to :py:meth:`parseconfigure`. + :param withinit: + Whether to also write an ``__init__.py`` file to the same + directory to ensure it is a package. """ if isinstance(source, Path): path = self.tmpdir.join(str(source)) @@ -1147,12 +1129,11 @@ def collect_by_name( ) -> Optional[Union[Item, Collector]]: """Return the collection node for name from the module collection. - This will search a module collection node for a collection node - matching the given name. - - :param modcol: a module collection node; see :py:meth:`getmodulecol` + Searchs a module collection node for a collection node matching the + given name. - :param name: the name of the node to return + :param modcol: A module collection node; see :py:meth:`getmodulecol`. + :param name: The name of the node to return. """ if modcol not in self._mod_collections: self._mod_collections[modcol] = list(modcol.collect()) @@ -1171,11 +1152,10 @@ def popen( ): """Invoke subprocess.Popen. - This calls subprocess.Popen making sure the current working directory - is in the PYTHONPATH. + Calls subprocess.Popen making sure the current working directory is + in the PYTHONPATH. You probably want to use :py:meth:`run` instead. - """ env = os.environ.copy() env["PYTHONPATH"] = os.pathsep.join( @@ -1207,16 +1187,18 @@ def run( Run a process using subprocess.Popen saving the stdout and stderr. - :param args: the sequence of arguments to pass to `subprocess.Popen()` - :kwarg timeout: the period in seconds after which to timeout and raise - :py:class:`Testdir.TimeoutExpired` - :kwarg stdin: optional standard input. Bytes are being send, closing + :param args: + The sequence of arguments to pass to `subprocess.Popen()`. + :param timeout: + The period in seconds after which to timeout and raise + :py:class:`Testdir.TimeoutExpired`. + :param stdin: + Optional standard input. Bytes are being send, closing the pipe, otherwise it is passed through to ``popen``. Defaults to ``CLOSE_STDIN``, which translates to using a pipe (``subprocess.PIPE``) that gets closed. - Returns a :py:class:`RunResult`. - + :rtype: RunResult """ __tracebackhide__ = True @@ -1292,13 +1274,15 @@ def _getpytestargs(self) -> Tuple[str, ...]: def runpython(self, script) -> RunResult: """Run a python script using sys.executable as interpreter. - Returns a :py:class:`RunResult`. - + :rtype: RunResult """ return self.run(sys.executable, script) def runpython_c(self, command): - """Run python -c "command", return a :py:class:`RunResult`.""" + """Run python -c "command". + + :rtype: RunResult + """ return self.run(sys.executable, "-c", command) def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunResult: @@ -1310,11 +1294,13 @@ def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunRes with "runpytest-" to not conflict with the normal numbered pytest location for temporary files and directories. - :param args: the sequence of arguments to pass to the pytest subprocess - :param timeout: the period in seconds after which to timeout and raise - :py:class:`Testdir.TimeoutExpired` + :param args: + The sequence of arguments to pass to the pytest subprocess. + :param timeout: + The period in seconds after which to timeout and raise + :py:class:`Testdir.TimeoutExpired`. - Returns a :py:class:`RunResult`. + :rtype: RunResult """ __tracebackhide__ = True p = make_numbered_dir(root=Path(str(self.tmpdir)), prefix="runpytest-") @@ -1334,7 +1320,6 @@ def spawn_pytest( directory locations. The pexpect child is returned. - """ basetemp = self.tmpdir.mkdir("temp-pexpect") invoke = " ".join(map(str, self._getpytestargs())) @@ -1345,7 +1330,6 @@ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": """Run a command using pexpect. The pexpect child is returned. - """ pexpect = pytest.importorskip("pexpect", "3.0") if hasattr(sys, "pypy_version_info") and "64" in platform.machine(): @@ -1400,14 +1384,12 @@ def _getlines(self, lines2: Union[str, Sequence[str], Source]) -> Sequence[str]: return lines2 def fnmatch_lines_random(self, lines2: Sequence[str]) -> None: - """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`). - """ + """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`).""" __tracebackhide__ = True self._match_lines_random(lines2, fnmatch) def re_match_lines_random(self, lines2: Sequence[str]) -> None: - """Check lines exist in the output in any order (using :func:`python:re.match`). - """ + """Check lines exist in the output in any order (using :func:`python:re.match`).""" __tracebackhide__ = True self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name))) @@ -1452,8 +1434,8 @@ def fnmatch_lines( wildcards. If they do not match a pytest.fail() is called. The matches and non-matches are also shown as part of the error message. - :param lines2: string patterns to match. - :param consecutive: match lines consecutive? + :param lines2: String patterns to match. + :param consecutive: Match lines consecutively? """ __tracebackhide__ = True self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive) @@ -1489,14 +1471,18 @@ def _match_lines( ) -> None: """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. - :param list[str] lines2: list of string patterns to match. The actual - format depends on ``match_func`` - :param match_func: a callable ``match_func(line, pattern)`` where line - is the captured line from stdout/stderr and pattern is the matching - pattern - :param str match_nickname: the nickname for the match function that - will be logged to stdout when a match occurs - :param consecutive: match lines consecutively? + :param Sequence[str] lines2: + List of string patterns to match. The actual format depends on + ``match_func``. + :param match_func: + A callable ``match_func(line, pattern)`` where line is the + captured line from stdout/stderr and pattern is the matching + pattern. + :param str match_nickname: + The nickname for the match function that will be logged to stdout + when a match occurs. + :param consecutive: + Match lines consecutively? """ if not isinstance(lines2, collections.abc.Sequence): raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__)) @@ -1546,7 +1532,7 @@ def _match_lines( def no_fnmatch_line(self, pat: str) -> None: """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``. - :param str pat: the pattern to match lines. + :param str pat: The pattern to match lines. """ __tracebackhide__ = True self._no_match_line(pat, fnmatch, "fnmatch") @@ -1554,7 +1540,7 @@ def no_fnmatch_line(self, pat: str) -> None: def no_re_match_line(self, pat: str) -> None: """Ensure captured lines do not match the given pattern, using ``re.match``. - :param str pat: the regular expression to match lines. + :param str pat: The regular expression to match lines. """ __tracebackhide__ = True self._no_match_line( @@ -1564,9 +1550,9 @@ def no_re_match_line(self, pat: str) -> None: def _no_match_line( self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str ) -> None: - """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch`` + """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``. - :param str pat: the pattern to match lines + :param str pat: The pattern to match lines. """ __tracebackhide__ = True nomatch_printed = False diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 50f03eadb15..589dfd06e27 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1,4 +1,4 @@ -""" Python test discovery, setup and run of test functions. """ +"""Python test discovery, setup and run of test functions.""" import enum import fnmatch import inspect @@ -201,7 +201,7 @@ def pytest_collect_file(path: py.path.local, parent) -> Optional["Module"]: def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool: - """Returns True if path matches any of the patterns in the list of globs given.""" + """Return whether path matches any of the patterns in the list of globs given.""" return any(path.fnmatch(pattern) for pattern in patterns) @@ -215,16 +215,16 @@ def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module": @hookimpl(trylast=True) def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object): - # nothing was collected elsewhere, let's do it here + # Nothing was collected elsewhere, let's do it here. if safe_isclass(obj): if collector.istestclass(obj, name): return Class.from_parent(collector, name=name, obj=obj) elif collector.istestfunction(obj, name): - # mock seems to store unbound methods (issue473), normalize it + # mock seems to store unbound methods (issue473), normalize it. obj = getattr(obj, "__func__", obj) # We need to try and unwrap the function if it's a functools.partial # or a functools.wrapped. - # We mustn't if it's been wrapped with mock.patch (python 2 only) + # We mustn't if it's been wrapped with mock.patch (python 2 only). if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))): filename, lineno = getfslineno(obj) warnings.warn_explicit( @@ -298,14 +298,14 @@ def obj(self, value): self._obj = value def _getobj(self): - """Gets the underlying Python object. May be overwritten by subclasses.""" + """Get the underlying Python object. May be overwritten by subclasses.""" # TODO: Improve the type of `parent` such that assert/ignore aren't needed. assert self.parent is not None obj = self.parent.obj # type: ignore[attr-defined] return getattr(obj, self.name) def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str: - """ return python path relative to the containing module. """ + """Return Python path relative to the containing module.""" chain = self.listchain() chain.reverse() parts = [] @@ -346,8 +346,8 @@ def funcnamefilter(self, name: str) -> bool: return self._matches_prefix_or_glob_option("python_functions", name) def isnosetest(self, obj: object) -> bool: - """ Look for the __test__ attribute, which is applied by the - @nose.tools.istest decorator + """Look for the __test__ attribute, which is applied by the + @nose.tools.istest decorator. """ # We explicitly check for "is True" here to not mistakenly treat # classes with a custom __getattr__ returning something truthy (like a @@ -360,7 +360,7 @@ def classnamefilter(self, name: str) -> bool: def istestfunction(self, obj: object, name: str) -> bool: if self.funcnamefilter(name) or self.isnosetest(obj): if isinstance(obj, staticmethod): - # static methods need to be unwrapped + # staticmethods need to be unwrapped. obj = safe_getattr(obj, "__func__", False) return ( safe_getattr(obj, "__call__", False) @@ -373,16 +373,14 @@ def istestclass(self, obj: object, name: str) -> bool: return self.classnamefilter(name) or self.isnosetest(obj) def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool: - """ - checks if the given name matches the prefix or glob-pattern defined - in ini configuration. - """ + """Check if the given name matches the prefix or glob-pattern defined + in ini configuration.""" for option in self.config.getini(option_name): if name.startswith(option): return True - # check that name looks like a glob-string before calling fnmatch + # Check that name looks like a glob-string before calling fnmatch # because this is called for every name in each collected module, - # and fnmatch is somewhat expensive to call + # and fnmatch is somewhat expensive to call. elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch( name, option ): @@ -457,10 +455,10 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]: if not metafunc._calls: yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) else: - # add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs + # Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs. fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm) - # add_funcarg_pseudo_fixture_def may have shadowed some fixtures + # Add_funcarg_pseudo_fixture_def may have shadowed some fixtures # with direct parametrization, so make sure we update what the # function really needs. fixtureinfo.prune_dependency_tree() @@ -479,7 +477,7 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]: class Module(nodes.File, PyCollector): - """ Collector for test classes and functions. """ + """Collector for test classes and functions.""" def _getobj(self): return self._importtestmodule() @@ -491,7 +489,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: return super().collect() def _inject_setup_module_fixture(self) -> None: - """Injects a hidden autouse, module scoped fixture into the collected module object + """Inject a hidden autouse, module scoped fixture into the collected module object that invokes setUpModule/tearDownModule if either or both are available. Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with @@ -518,7 +516,7 @@ def xunit_setup_module_fixture(request) -> Generator[None, None, None]: self.obj.__pytest_setup_module = xunit_setup_module_fixture def _inject_setup_function_fixture(self) -> None: - """Injects a hidden autouse, function scoped fixture into the collected module object + """Inject a hidden autouse, function scoped fixture into the collected module object that invokes setup_function/teardown_function if either or both are available. Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with @@ -547,7 +545,7 @@ def xunit_setup_function_fixture(request) -> Generator[None, None, None]: self.obj.__pytest_setup_function = xunit_setup_function_fixture def _importtestmodule(self): - # we assume we are only called once per module + # We assume we are only called once per module. importmode = self.config.getoption("--import-mode") try: mod = import_path(self.fspath, mode=importmode) @@ -604,7 +602,7 @@ def __init__( session=None, nodeid=None, ) -> None: - # NOTE: could be just the following, but kept as-is for compat. + # NOTE: Could be just the following, but kept as-is for compat. # nodes.FSCollector.__init__(self, fspath, parent=parent) session = parent.session nodes.FSCollector.__init__( @@ -613,8 +611,8 @@ def __init__( self.name = os.path.basename(str(fspath.dirname)) def setup(self) -> None: - # not using fixtures to call setup_module here because autouse fixtures - # from packages are not called automatically (#4085) + # Not using fixtures to call setup_module here because autouse fixtures + # from packages are not called automatically (#4085). setup_module = _get_first_non_fixture_func( self.obj, ("setUpModule", "setup_module") ) @@ -668,7 +666,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: def _call_with_optional_argument(func, arg) -> None: """Call the given function with the given argument if func accepts one argument, otherwise - calls func without arguments""" + calls func without arguments.""" arg_count = func.__code__.co_argcount if inspect.ismethod(func): arg_count -= 1 @@ -680,9 +678,7 @@ def _call_with_optional_argument(func, arg) -> None: def _get_first_non_fixture_func(obj: object, names: Iterable[str]): """Return the attribute from the given object to be used as a setup/teardown - xunit-style function, but only if not marked as a fixture to - avoid calling it twice. - """ + xunit-style function, but only if not marked as a fixture to avoid calling it twice.""" for name in names: meth = getattr(obj, name, None) if meth is not None and fixtures.getfixturemarker(meth) is None: @@ -690,13 +686,11 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]): class Class(PyCollector): - """ Collector for test methods. """ + """Collector for test methods.""" @classmethod def from_parent(cls, parent, *, name, obj=None): - """ - The public constructor - """ + """The public constructor.""" return super().from_parent(name=name, parent=parent) def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: @@ -729,7 +723,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: return [Instance.from_parent(self, name="()")] def _inject_setup_class_fixture(self) -> None: - """Injects a hidden autouse, class scoped fixture into the collected class object + """Inject a hidden autouse, class scoped fixture into the collected class object that invokes setup_class/teardown_class if either or both are available. Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with @@ -753,7 +747,7 @@ def xunit_setup_class_fixture(cls) -> Generator[None, None, None]: self.obj.__pytest_setup_class = xunit_setup_class_fixture def _inject_setup_method_fixture(self) -> None: - """Injects a hidden autouse, function scoped fixture into the collected class object + """Inject a hidden autouse, function scoped fixture into the collected class object that invokes setup_method/teardown_method if either or both are available. Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with @@ -780,9 +774,9 @@ def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]: class Instance(PyCollector): _ALLOW_MARKERS = False # hack, destroy later - # instances share the object with their parents in a way + # Instances share the object with their parents in a way # that duplicates markers instances if not taken out - # can be removed at node structure reorganization time + # can be removed at node structure reorganization time. def _getobj(self): # TODO: Improve the type of `parent` such that assert/ignore aren't needed. @@ -874,8 +868,8 @@ def setmulti2( class Metafunc: - """ - Metafunc objects are passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook. + """Objects passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook. + They help to inspect a test function and to generate tests according to test configuration or values specified in the class or module where a test function is defined. @@ -891,19 +885,19 @@ def __init__( ) -> None: self.definition = definition - #: access to the :class:`_pytest.config.Config` object for the test session + #: Access to the :class:`_pytest.config.Config` object for the test session. self.config = config - #: the module object where the test function is defined in. + #: The module object where the test function is defined in. self.module = module - #: underlying python test function + #: Underlying Python test function. self.function = definition.obj - #: set of fixture names required by the test function + #: Set of fixture names required by the test function. self.fixturenames = fixtureinfo.names_closure - #: class object where the test function is defined in or ``None``. + #: Class object where the test function is defined in or ``None``. self.cls = cls self._calls = [] # type: List[CallSpec2] @@ -911,7 +905,7 @@ def __init__( @property def funcargnames(self) -> List[str]: - """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + """Alias attribute for ``fixturenames`` for pre-2.3 compatibility.""" warnings.warn(FUNCARGNAMES, stacklevel=2) return self.fixturenames @@ -930,30 +924,35 @@ def parametrize( *, _param_mark: Optional[Mark] = None ) -> None: - """ Add new invocations to the underlying test function using the list + """Add new invocations to the underlying test function using the list of argvalues for the given argnames. Parametrization is performed during the collection phase. If you need to setup expensive resources see about setting indirect to do it rather at test setup time. - :arg argnames: a comma-separated string denoting one or more argument - names, or a list/tuple of argument strings. + :param argnames: + A comma-separated string denoting one or more argument names, or + a list/tuple of argument strings. + + :param argvalues: + The list of argvalues determines how often a test is invoked with + different argument values. - :arg argvalues: The list of argvalues determines how often a - test is invoked with different argument values. If only one - argname was specified argvalues is a list of values. If N - argnames were specified, argvalues must be a list of N-tuples, - where each tuple-element specifies a value for its respective - argname. + If only one argname was specified argvalues is a list of values. + If N argnames were specified, argvalues must be a list of + N-tuples, where each tuple-element specifies a value for its + respective argname. - :arg indirect: The list of argnames or boolean. A list of arguments' - names (subset of argnames). If True the list contains all names from - the argnames. Each argvalue corresponding to an argname in this list will + :param indirect: + A list of arguments' names (subset of argnames) or a boolean. + If True the list contains all names from the argnames. Each + argvalue corresponding to an argname in this list will be passed as request.param to its respective argname fixture function so that it can perform more expensive setups during the setup phase of a test rather than at collection time. - :arg ids: sequence of (or generator for) ids for ``argvalues``, - or a callable to return part of the id for each argvalue. + :param ids: + Sequence of (or generator for) ids for ``argvalues``, + or a callable to return part of the id for each argvalue. With sequences (and generators like ``itertools.count()``) the returned ids should be of type ``string``, ``int``, ``float``, @@ -971,7 +970,8 @@ def parametrize( If no ids are provided they will be generated automatically from the argvalues. - :arg scope: if specified it denotes the scope of the parameters. + :param scope: + If specified it denotes the scope of the parameters. The scope is used for grouping tests by parameter instances. It will also override any fixture-function defined scope, allowing to set a dynamic scope using test context or configuration. @@ -1018,9 +1018,9 @@ def parametrize( scope, descr="parametrize() call in {}".format(self.function.__name__) ) - # create the new calls: if we are parametrize() multiple times (by applying the decorator + # Create the new calls: if we are parametrize() multiple times (by applying the decorator # more than once) then we accumulate those calls generating the cartesian product - # of all calls + # of all calls. newcalls = [] for callspec in self._calls or [CallSpec2(self)]: for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)): @@ -1049,15 +1049,15 @@ def _resolve_arg_ids( parameters: typing.Sequence[ParameterSet], nodeid: str, ) -> List[str]: - """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given + """Resolve the actual ids for the given argnames, based on the ``ids`` parameter given to ``parametrize``. - :param List[str] argnames: list of argument names passed to ``parametrize()``. - :param ids: the ids parameter of the parametrized call (see docs). - :param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``. - :param str str: the nodeid of the item that generated this parametrized call. + :param List[str] argnames: List of argument names passed to ``parametrize()``. + :param ids: The ids parameter of the parametrized call (see docs). + :param List[ParameterSet] parameters: The list of parameter values, same size as ``argnames``. + :param str str: The nodeid of the item that generated this parametrized call. :rtype: List[str] - :return: the list of ids for each argname given + :returns: The list of ids for each argname given. """ if ids is None: idfn = None @@ -1109,11 +1109,12 @@ def _resolve_arg_value_types( argnames: typing.Sequence[str], indirect: Union[bool, typing.Sequence[str]], ) -> Dict[str, "Literal['params', 'funcargs']"]: - """Resolves if each parametrized argument must be considered a parameter to a fixture or a "funcarg" - to the function, based on the ``indirect`` parameter of the parametrized() call. + """Resolve if each parametrized argument must be considered a + parameter to a fixture or a "funcarg" to the function, based on the + ``indirect`` parameter of the parametrized() call. - :param List[str] argnames: list of argument names passed to ``parametrize()``. - :param indirect: same ``indirect`` parameter of ``parametrize()``. + :param List[str] argnames: List of argument names passed to ``parametrize()``. + :param indirect: Same as the ``indirect`` parameter of ``parametrize()``. :rtype: Dict[str, str] A dict mapping each arg name to either: * "params" if the argname should be the parameter of a fixture of the same name. @@ -1148,12 +1149,11 @@ def _validate_if_using_arg_names( argnames: typing.Sequence[str], indirect: Union[bool, typing.Sequence[str]], ) -> None: - """ - Check if all argnames are being used, by default values, or directly/indirectly. + """Check if all argnames are being used, by default values, or directly/indirectly. - :param List[str] argnames: list of argument names passed to ``parametrize()``. - :param indirect: same ``indirect`` parameter of ``parametrize()``. - :raise ValueError: if validation fails. + :param List[str] argnames: List of argument names passed to ``parametrize()``. + :param indirect: Same as the ``indirect`` parameter of ``parametrize()``. + :raises ValueError: If validation fails. """ default_arg_names = set(get_default_arg_names(self.function)) func_name = self.function.__name__ @@ -1204,7 +1204,7 @@ def _find_parametrized_scope( if name in argnames ] if used_scopes: - # Takes the most narrow scope from used fixtures + # Takes the most narrow scope from used fixtures. for scope in reversed(fixtures.scopes): if scope in used_scopes: return scope @@ -1259,7 +1259,7 @@ def _idval( elif isinstance(val, enum.Enum): return str(val) elif isinstance(getattr(val, "__name__", None), str): - # name of a class, function, module, etc. + # Name of a class, function, module, etc. name = getattr(val, "__name__") # type: str return name return str(argname) + str(idx) @@ -1306,13 +1306,13 @@ def idmaker( unique_ids = set(resolved_ids) if len(unique_ids) != len(resolved_ids): - # Record the number of occurrences of each test ID + # Record the number of occurrences of each test ID. test_id_counts = Counter(resolved_ids) - # Map the test ID to its next suffix + # Map the test ID to its next suffix. test_id_suffixes = defaultdict(int) # type: Dict[str, int] - # Suffix non-unique IDs to make them unique + # Suffix non-unique IDs to make them unique. for index, test_id in enumerate(resolved_ids): if test_id_counts[test_id] > 1: resolved_ids[index] = "{}{}".format(test_id, test_id_suffixes[test_id]) @@ -1365,12 +1365,12 @@ def write_item(item: nodes.Item) -> None: tw.sep("-", "fixtures used by {}".format(item.name)) # TODO: Fix this type ignore. tw.sep("-", "({})".format(get_best_relpath(item.function))) # type: ignore[attr-defined] - # dict key not used in loop but needed for sorting + # dict key not used in loop but needed for sorting. for _, fixturedefs in sorted(info.name2fixturedefs.items()): assert fixturedefs is not None if not fixturedefs: continue - # last item is expected to be the one used by the test item + # Last item is expected to be the one used by the test item. write_fixture(fixturedefs[-1]) for session_item in session.items: @@ -1446,11 +1446,35 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: class Function(PyobjMixin, nodes.Item): - """ a Function Item is responsible for setting up and executing a - Python test function. + """An Item responsible for setting up and executing a Python test function. + + param name: + The full function name, including any decorations like those + added by parametrization (``my_func[my_param]``). + param parent: + The parent Node. + param config: + The pytest Config object. + param callspec: + If given, this is function has been parametrized and the callspec contains + meta information about the parametrization. + param callobj: + If given, the object which will be called when the Function is invoked, + otherwise the callobj will be obtained from ``parent`` using ``originalname``. + param keywords: + Keywords bound to the function object for "-k" matching. + param session: + The pytest Session object. + param fixtureinfo: + Fixture information already resolved at this fixture node.. + param originalname: + The attribute name to use for accessing the underlying function object. + Defaults to ``name``. Set this if name is different from the original name, + for example when it contains decorations like those added by parametrization + (``my_func[my_param]``). """ - # disable since functions handle it themselves + # Disable since functions handle it themselves. _ALLOW_MARKERS = False def __init__( @@ -1465,24 +1489,6 @@ def __init__( fixtureinfo: Optional[FuncFixtureInfo] = None, originalname: Optional[str] = None, ) -> None: - """ - param name: the full function name, including any decorations like those - added by parametrization (``my_func[my_param]``). - param parent: the parent Node. - param config: the pytest Config object - param callspec: if given, this is function has been parametrized and the callspec contains - meta information about the parametrization. - param callobj: if given, the object which will be called when the Function is invoked, - otherwise the callobj will be obtained from ``parent`` using ``originalname`` - param keywords: keywords bound to the function object for "-k" matching. - param session: the pytest Session object - param fixtureinfo: fixture information already resolved at this fixture node. - param originalname: - The attribute name to use for accessing the underlying function object. - Defaults to ``name``. Set this if name is different from the original name, - for example when it contains decorations like those added by parametrization - (``my_func[my_param]``). - """ super().__init__(name, parent, config=config, session=session) if callobj is not NOTSET: @@ -1496,8 +1502,8 @@ def __init__( #: .. versionadded:: 3.0 self.originalname = originalname or name - # note: when FunctionDefinition is introduced, we should change ``originalname`` - # to a readonly property that returns FunctionDefinition.name + # Note: when FunctionDefinition is introduced, we should change ``originalname`` + # to a readonly property that returns FunctionDefinition.name. self.keywords.update(self.obj.__dict__) self.own_markers.extend(get_unpacked_marks(self.obj)) @@ -1535,9 +1541,7 @@ def __init__( @classmethod def from_parent(cls, parent, **kw): # todo: determine sound type limitations - """ - The public constructor - """ + """The public constructor.""" return super().from_parent(parent=parent, **kw) def _initrequest(self) -> None: @@ -1546,7 +1550,7 @@ def _initrequest(self) -> None: @property def function(self): - "underlying python 'function' object" + """Underlying python 'function' object.""" return getimfunc(self.obj) def _getobj(self): @@ -1555,17 +1559,17 @@ def _getobj(self): @property def _pyfuncitem(self): - "(compatonly) for code expecting pytest-2.2 style request objects" + """(compatonly) for code expecting pytest-2.2 style request objects.""" return self @property def funcargnames(self) -> List[str]: - """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + """Alias attribute for ``fixturenames`` for pre-2.3 compatibility.""" warnings.warn(FUNCARGNAMES, stacklevel=2) return self.fixturenames def runtest(self) -> None: - """ execute the underlying test function. """ + """Execute the underlying test function.""" self.ihook.pytest_pyfunc_call(pyfuncitem=self) def setup(self) -> None: @@ -1589,7 +1593,7 @@ def _prunetraceback(self, excinfo: ExceptionInfo) -> None: excinfo.traceback = ntraceback.filter() # issue364: mark all but first and last frames to - # only show a single-line message for each frame + # only show a single-line message for each frame. if self.config.getoption("tbstyle", "auto") == "auto": if len(excinfo.traceback) > 2: for entry in excinfo.traceback[1:-1]: @@ -1606,10 +1610,8 @@ def repr_failure( # type: ignore[override] class FunctionDefinition(Function): - """ - internal hack until we get actual definition nodes instead of the - crappy metafunc hack - """ + """Internal hack until we get actual definition nodes instead of the + crappy metafunc hack.""" def runtest(self) -> None: raise RuntimeError("function definitions are not supposed to be used") diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index fb6c76852d2..1bad5c777b6 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -39,10 +39,8 @@ def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: class ApproxBase: - """ - Provide shared utilities for making approximate comparisons between numbers - or sequences of numbers. - """ + """Provide shared utilities for making approximate comparisons between + numbers or sequences of numbers.""" # Tell numpy to use our `__eq__` operator instead of its. __array_ufunc__ = None @@ -74,16 +72,14 @@ def _approx_scalar(self, x) -> "ApproxScalar": return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) def _yield_comparisons(self, actual): - """ - Yield all the pairs of numbers to be compared. This is used to - implement the `__eq__` method. + """Yield all the pairs of numbers to be compared. + + This is used to implement the `__eq__` method. """ raise NotImplementedError def _check_type(self) -> None: - """ - Raise a TypeError if the expected value is not a valid type. - """ + """Raise a TypeError if the expected value is not a valid type.""" # This is only a concern if the expected value is a sequence. In every # other case, the approx() function ensures that the expected value has # a numeric type. For this reason, the default is to do nothing. The @@ -100,9 +96,7 @@ def _recursive_list_map(f, x): class ApproxNumpy(ApproxBase): - """ - Perform approximate comparisons where the expected value is numpy array. - """ + """Perform approximate comparisons where the expected value is numpy array.""" def __repr__(self) -> str: list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist()) @@ -111,7 +105,7 @@ def __repr__(self) -> str: def __eq__(self, actual) -> bool: import numpy as np - # self.expected is supposed to always be an array here + # self.expected is supposed to always be an array here. if not np.isscalar(actual): try: @@ -142,10 +136,8 @@ def _yield_comparisons(self, actual): class ApproxMapping(ApproxBase): - """ - Perform approximate comparisons where the expected value is a mapping with - numeric values (the keys can be anything). - """ + """Perform approximate comparisons where the expected value is a mapping + with numeric values (the keys can be anything).""" def __repr__(self) -> str: return "approx({!r})".format( @@ -173,10 +165,7 @@ def _check_type(self) -> None: class ApproxSequencelike(ApproxBase): - """ - Perform approximate comparisons where the expected value is a sequence of - numbers. - """ + """Perform approximate comparisons where the expected value is a sequence of numbers.""" def __repr__(self) -> str: seq_type = type(self.expected) @@ -207,9 +196,7 @@ def _check_type(self) -> None: class ApproxScalar(ApproxBase): - """ - Perform approximate comparisons where the expected value is a single number. - """ + """Perform approximate comparisons where the expected value is a single number.""" # Using Real should be better than this Union, but not possible yet: # https://github.com/python/typeshed/pull/3108 @@ -217,13 +204,14 @@ class ApproxScalar(ApproxBase): DEFAULT_RELATIVE_TOLERANCE = 1e-6 # type: Union[float, Decimal] def __repr__(self) -> str: - """ - Return a string communicating both the expected value and the tolerance - for the comparison being made, e.g. '1.0 ± 1e-6', '(3+4j) ± 5e-6 ∠ ±180°'. + """Return a string communicating both the expected value and the + tolerance for the comparison being made. + + For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``. """ # Infinities aren't compared using tolerances, so don't show a - # tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j) + # tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j). if math.isinf(abs(self.expected)): return str(self.expected) @@ -239,10 +227,8 @@ def __repr__(self) -> str: return "{} ± {}".format(self.expected, vetted_tolerance) def __eq__(self, actual) -> bool: - """ - Return true if the given value is equal to the expected value within - the pre-specified tolerance. - """ + """Return whether the given value is equal to the expected value + within the pre-specified tolerance.""" if _is_numpy_array(actual): # Call ``__eq__()`` manually to prevent infinite-recursion with # numpy<1.13. See #3748. @@ -276,10 +262,10 @@ def __eq__(self, actual) -> bool: @property def tolerance(self): - """ - Return the tolerance for the comparison. This could be either an - absolute tolerance or a relative tolerance, depending on what the user - specified or which would be larger. + """Return the tolerance for the comparison. + + This could be either an absolute tolerance or a relative tolerance, + depending on what the user specified or which would be larger. """ def set_default(x, default): @@ -323,17 +309,14 @@ def set_default(x, default): class ApproxDecimal(ApproxScalar): - """ - Perform approximate comparisons where the expected value is a decimal. - """ + """Perform approximate comparisons where the expected value is a Decimal.""" DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12") DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6") def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: - """ - Assert that two numbers (or two sets of numbers) are equal to each other + """Assert that two numbers (or two sets of numbers) are equal to each other within some tolerance. Due to the `intricacies of floating-point arithmetic`__, numbers that we @@ -522,9 +505,9 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: def _is_numpy_array(obj: object) -> bool: - """ - Return true if the given object is a numpy array. Make a special effort to - avoid importing numpy unless it's really necessary. + """Return true if the given object is a numpy array. + + A special effort is made to avoid importing numpy unless it's really necessary. """ import sys @@ -563,11 +546,11 @@ def raises( # noqa: F811 *args: Any, **kwargs: Any ) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]: - r""" - Assert that a code block/function call raises ``expected_exception`` + r"""Assert that a code block/function call raises ``expected_exception`` or raise a failure exception otherwise. - :kwparam match: if specified, a string containing a regular expression, + :kwparam match: + If specified, a string containing a regular expression, or a regular expression object, that is tested against the string representation of the exception using ``re.search``. To match a literal string that may contain `special characters`__, the pattern can diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 11ca571aadd..ded414ab466 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -1,4 +1,4 @@ -""" recording warnings during test function execution. """ +"""Record warnings during test function execution.""" import re import warnings from types import TracebackType diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index cbd9ae1832a..65098343bf2 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -94,9 +94,8 @@ def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]: @property def longreprtext(self) -> str: - """ - Read-only property that returns the full string representation - of ``longrepr``. + """Read-only property that returns the full string representation of + ``longrepr``. .. versionadded:: 3.0 """ @@ -109,7 +108,7 @@ def longreprtext(self) -> str: @property def caplog(self) -> str: - """Return captured log lines, if log capturing is enabled + """Return captured log lines, if log capturing is enabled. .. versionadded:: 3.5 """ @@ -119,7 +118,7 @@ def caplog(self) -> str: @property def capstdout(self) -> str: - """Return captured text from stdout, if capturing is enabled + """Return captured text from stdout, if capturing is enabled. .. versionadded:: 3.0 """ @@ -129,7 +128,7 @@ def capstdout(self) -> str: @property def capstderr(self) -> str: - """Return captured text from stderr, if capturing is enabled + """Return captured text from stderr, if capturing is enabled. .. versionadded:: 3.0 """ @@ -147,11 +146,8 @@ def fspath(self) -> str: @property def count_towards_summary(self) -> bool: - """ - **Experimental** - - ``True`` if this report should be counted towards the totals shown at the end of the - test session: "1 passed, 1 failure, etc". + """**Experimental** Whether this report should be counted towards the + totals shown at the end of the test session: "1 passed, 1 failure, etc". .. note:: @@ -162,11 +158,9 @@ def count_towards_summary(self) -> bool: @property def head_line(self) -> Optional[str]: - """ - **Experimental** - - Returns the head line shown with longrepr output for this report, more commonly during - traceback representation during failures:: + """**Experimental** The head line shown with longrepr output for this + report, more commonly during traceback representation during + failures:: ________ Test.foo ________ @@ -190,11 +184,10 @@ def _get_verbose_word(self, config: Config): return verbose def _to_json(self) -> Dict[str, Any]: - """ - This was originally the serialize_report() function from xdist (ca03269). + """Return the contents of this report as a dict of builtin entries, + suitable for serialization. - Returns the contents of this report as a dict of builtin entries, suitable for - serialization. + This was originally the serialize_report() function from xdist (ca03269). Experimental method. """ @@ -202,11 +195,11 @@ def _to_json(self) -> Dict[str, Any]: @classmethod def _from_json(cls: "Type[_R]", reportdict: Dict[str, object]) -> _R: - """ - This was originally the serialize_report() function from xdist (ca03269). + """Create either a TestReport or CollectReport, depending on the calling class. + + It is the callers responsibility to know which class to pass here. - Factory method that returns either a TestReport or CollectReport, depending on the calling - class. It's the callers responsibility to know which class to pass here. + This was originally the serialize_report() function from xdist (ca03269). Experimental method. """ @@ -229,9 +222,8 @@ def _report_unserialization_failure( class TestReport(BaseReport): - """ Basic test report object (also used for setup and teardown calls if - they fail). - """ + """Basic test report object (also used for setup and teardown calls if + they fail).""" __test__ = False @@ -248,38 +240,38 @@ def __init__( user_properties: Optional[Iterable[Tuple[str, object]]] = None, **extra ) -> None: - #: normalized collection node id + #: Normalized collection nodeid. self.nodeid = nodeid - #: a (filesystempath, lineno, domaininfo) tuple indicating the + #: A (filesystempath, lineno, domaininfo) tuple indicating the #: actual location of a test item - it might be different from the #: collected one e.g. if a method is inherited from a different module. self.location = location # type: Tuple[str, Optional[int], str] - #: a name -> value dictionary containing all keywords and + #: A name -> value dictionary containing all keywords and #: markers associated with a test invocation. self.keywords = keywords - #: test outcome, always one of "passed", "failed", "skipped". + #: Test outcome, always one of "passed", "failed", "skipped". self.outcome = outcome #: None or a failure representation. self.longrepr = longrepr - #: one of 'setup', 'call', 'teardown' to indicate runtest phase. + #: One of 'setup', 'call', 'teardown' to indicate runtest phase. self.when = when - #: user properties is a list of tuples (name, value) that holds user - #: defined properties of the test + #: User properties is a list of tuples (name, value) that holds user + #: defined properties of the test. self.user_properties = list(user_properties or []) - #: list of pairs ``(str, str)`` of extra information which needs to + #: List of pairs ``(str, str)`` of extra information which needs to #: marshallable. Used by pytest to add captured text #: from ``stdout`` and ``stderr``, but may be used by other plugins #: to add arbitrary information to reports. self.sections = list(sections) - #: time it took to run just the test + #: Time it took to run just the test. self.duration = duration self.__dict__.update(extra) @@ -291,9 +283,7 @@ def __repr__(self) -> str: @classmethod def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": - """ - Factory method to create and fill a TestReport with standard item and call info. - """ + """Create and fill a TestReport with standard item and call info.""" when = call.when # Remove "collect" from the Literal type -- only for collection calls. assert when != "collect" @@ -350,10 +340,10 @@ def __init__( sections: Iterable[Tuple[str, str]] = (), **extra ) -> None: - #: normalized collection node id + #: Normalized collection nodeid. self.nodeid = nodeid - #: test outcome, always one of "passed", "failed", "skipped". + #: Test outcome, always one of "passed", "failed", "skipped". self.outcome = outcome #: None or a failure representation. @@ -362,10 +352,11 @@ def __init__( #: The collected items and collection nodes. self.result = result or [] - #: list of pairs ``(str, str)`` of extra information which needs to - #: marshallable. Used by pytest to add captured text - #: from ``stdout`` and ``stderr``, but may be used by other plugins - #: to add arbitrary information to reports. + #: List of pairs ``(str, str)`` of extra information which needs to + #: marshallable. + # Used by pytest to add captured text : from ``stdout`` and ``stderr``, + # but may be used by other plugins : to add arbitrary information to + # reports. self.sections = list(sections) self.__dict__.update(extra) @@ -413,11 +404,10 @@ def pytest_report_from_serializable( def _report_to_json(report: BaseReport) -> Dict[str, Any]: - """ - This was originally the serialize_report() function from xdist (ca03269). + """Return the contents of this report as a dict of builtin entries, + suitable for serialization. - Returns the contents of this report as a dict of builtin entries, suitable for - serialization. + This was originally the serialize_report() function from xdist (ca03269). """ def serialize_repr_entry( @@ -485,10 +475,10 @@ def serialize_longrepr(rep: BaseReport) -> Dict[str, Any]: def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]: - """ - This was originally the serialize_report() function from xdist (ca03269). + """Return **kwargs that can be used to construct a TestReport or + CollectReport instance. - Returns **kwargs that can be used to construct a TestReport or CollectReport instance. + This was originally the serialize_report() function from xdist (ca03269). """ def deserialize_repr_entry(entry_data): diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index cd6824abfc5..356a39c1217 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -1,6 +1,4 @@ -""" log machine-parseable test session result information in a plain -text file. -""" +"""log machine-parseable test session result information to a plain text file.""" import os import py @@ -30,7 +28,7 @@ def pytest_addoption(parser: Parser) -> None: def pytest_configure(config: Config) -> None: resultlog = config.option.resultlog - # prevent opening resultlog on worker nodes (xdist) + # Prevent opening resultlog on worker nodes (xdist). if resultlog and not hasattr(config, "workerinput"): dirname = os.path.dirname(os.path.abspath(resultlog)) if not os.path.isdir(dirname): diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 69754ad5e10..289d676d607 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -1,4 +1,4 @@ -""" basic collect and runtest protocol implementations """ +"""Basic collect and runtest protocol implementations.""" import bdb import os import sys @@ -39,7 +39,7 @@ from _pytest.terminal import TerminalReporter # -# pytest plugin hooks +# pytest plugin hooks. def pytest_addoption(parser: Parser) -> None: @@ -116,8 +116,8 @@ def runtestprotocol( if not item.config.getoption("setuponly", False): reports.append(call_and_report(item, "call", log)) reports.append(call_and_report(item, "teardown", log, nextitem=nextitem)) - # after all teardown hooks have been called - # want funcargs and request info to go away + # After all teardown hooks have been called + # want funcargs and request info to go away. if hasrequest: item._request = False # type: ignore[attr-defined] item.funcargs = None # type: ignore[attr-defined] @@ -170,8 +170,7 @@ def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None: def _update_current_test_var( item: Item, when: Optional["Literal['setup', 'call', 'teardown']"] ) -> None: - """ - Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage. + """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage. If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment. """ @@ -253,15 +252,21 @@ def call_runtest_hook( @attr.s(repr=False) class CallInfo(Generic[_T]): - """ Result/Exception info a function invocation. - - :param T result: The return value of the call, if it didn't raise. Can only be accessed - if excinfo is None. - :param Optional[ExceptionInfo] excinfo: The captured exception of the call, if it raised. - :param float start: The system time when the call started, in seconds since the epoch. - :param float stop: The system time when the call ended, in seconds since the epoch. - :param float duration: The call duration, in seconds. - :param str when: The context of invocation: "setup", "call", "teardown", ... + """Result/Exception info a function invocation. + + :param T result: + The return value of the call, if it didn't raise. Can only be + accessed if excinfo is None. + :param Optional[ExceptionInfo] excinfo: + The captured exception of the call, if it raised. + :param float start: + The system time when the call started, in seconds since the epoch. + :param float stop: + The system time when the call ended, in seconds since the epoch. + :param float duration: + The call duration, in seconds. + :param str when: + The context of invocation: "setup", "call", "teardown", ... """ _result = attr.ib(type="Optional[_T]") @@ -352,14 +357,14 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: class SetupState: - """ shared state for setting up/tearing down test items or collectors. """ + """Shared state for setting up/tearing down test items or collectors.""" def __init__(self): self.stack = [] # type: List[Node] self._finalizers = {} # type: Dict[Node, List[Callable[[], object]]] def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None: - """ attach a finalizer to the given colitem. """ + """Attach a finalizer to the given colitem.""" assert colitem and not isinstance(colitem, tuple) assert callable(finalizer) # assert colitem in self.stack # some unit tests don't setup stack :/ @@ -419,7 +424,7 @@ def _teardown_towards(self, needed_collectors) -> None: def prepare(self, colitem) -> None: """Setup objects along the collector chain to the test-method.""" - # check if the last collection node has raised an error + # Check if the last collection node has raised an error. for col in self.stack: if hasattr(col, "_prepare_exc"): exc = col._prepare_exc # type: ignore[attr-defined] diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index e333e78df9b..c5b4ff39e85 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -1,4 +1,4 @@ -""" support for skip/xfail functions and markers. """ +"""Support for skip/xfail functions and markers.""" import os import platform import sys @@ -298,9 +298,9 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): and rep.skipped and type(rep.longrepr) is tuple ): - # skipped by mark.skipif; change the location of the failure + # Skipped by mark.skipif; change the location of the failure # to point to the item definition, otherwise it will display - # the location of where the skip exception was raised within pytest + # the location of where the skip exception was raised within pytest. _, _, reason = rep.longrepr filename, line = item.reportinfo()[:2] assert line is not None diff --git a/src/_pytest/store.py b/src/_pytest/store.py index 2b46c438936..fbf3c588f36 100644 --- a/src/_pytest/store.py +++ b/src/_pytest/store.py @@ -92,7 +92,7 @@ def __setitem__(self, key: StoreKey[T], value: T) -> None: def __getitem__(self, key: StoreKey[T]) -> T: """Get the value for key. - Raises KeyError if the key wasn't set before. + Raises ``KeyError`` if the key wasn't set before. """ return cast(T, self._store[key]) @@ -116,10 +116,10 @@ def setdefault(self, key: StoreKey[T], default: T) -> T: def __delitem__(self, key: StoreKey[T]) -> None: """Delete the value for key. - Raises KeyError if the key wasn't set before. + Raises ``KeyError`` if the key wasn't set before. """ del self._store[key] def __contains__(self, key: StoreKey[T]) -> bool: - """Returns whether key was set.""" + """Return whether key was set.""" return key in self._store diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index cbca9ba465a..86c327226d9 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1,4 +1,4 @@ -""" terminal reporting of the full testing process. +"""Terminal reporting of the full testing process. This is a good source for looking at the various reporting hooks. """ @@ -69,11 +69,10 @@ class MoreQuietAction(argparse.Action): - """ - a modified copy of the argparse count action which counts down and updates - the legacy quiet attribute at the same time + """A modified copy of the argparse count action which counts down and updates + the legacy quiet attribute at the same time. - used to unify verbosity handling + Used to unify verbosity handling. """ def __init__( @@ -276,13 +275,14 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]: @attr.s class WarningReport: - """ - Simple structure to hold warnings information captured by ``pytest_warning_recorded``. + """Simple structure to hold warnings information captured by ``pytest_warning_recorded``. - :ivar str message: user friendly message about the warning - :ivar str|None nodeid: node id that generated the warning (see ``get_location``). + :ivar str message: + User friendly message about the warning. + :ivar str|None nodeid: + nodeid that generated the warning (see ``get_location``). :ivar tuple|py.path.local fslocation: - file system location of the source of the warning (see ``get_location``). + File system location of the source of the warning (see ``get_location``). """ message = attr.ib(type=str) @@ -293,10 +293,7 @@ class WarningReport: count_towards_summary = True def get_location(self, config: Config) -> Optional[str]: - """ - Returns the more user-friendly information about the location - of a warning, or None. - """ + """Return the more user-friendly information about the location of a warning, or None.""" if self.nodeid: return self.nodeid if self.fslocation: @@ -349,7 +346,7 @@ def writer(self, value: TerminalWriter) -> None: self._tw = value def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]": - """Return True if we should display progress information based on the current config""" + """Return whether we should display progress information based on the current config.""" # do not show progress if we are not capturing output (#3038) if self.config.getoption("capture", "no") == "no": return False @@ -439,10 +436,10 @@ def write_line(self, line: Union[str, bytes], **markup: bool) -> None: self._tw.line(line, **markup) def rewrite(self, line: str, **markup: bool) -> None: - """ - Rewinds the terminal cursor to the beginning and writes the given line. + """Rewinds the terminal cursor to the beginning and writes the given line. - :kwarg erase: if True, will also add spaces until the full terminal width to ensure + :param erase: + If True, will also add spaces until the full terminal width to ensure previous lines are properly erased. The rest of the keyword arguments are markup instructions. @@ -499,9 +496,9 @@ def pytest_warning_recorded( def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: if self.config.option.traceconfig: msg = "PLUGIN registered: {}".format(plugin) - # XXX this event may happen during setup/teardown time + # XXX This event may happen during setup/teardown time # which unfortunately captures our output here - # which garbles our output if we use self.write_line + # which garbles our output if we use self.write_line. self.write_line(msg) def pytest_deselected(self, items: Sequence[Item]) -> None: @@ -510,8 +507,8 @@ def pytest_deselected(self, items: Sequence[Item]) -> None: def pytest_runtest_logstart( self, nodeid: str, location: Tuple[str, Optional[int], str] ) -> None: - # ensure that the path is printed before the - # 1st test of a module starts running + # Ensure that the path is printed before the + # 1st test of a module starts running. if self.showlongtestinfo: line = self._locationline(nodeid, *location) self.write_ensure_prefix(line, "") @@ -533,7 +530,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: markup = None self._add_stats(category, [rep]) if not letter and not word: - # probably passed setup/teardown + # Probably passed setup/teardown. return running_xdist = hasattr(rep, "node") if markup is None: @@ -623,7 +620,7 @@ def _write_progress_information_filling_space(self) -> None: @property def _width_of_current_line(self) -> int: - """Return the width of current line, using the superior implementation of py-1.6 when available""" + """Return the width of the current line.""" return self._tw.width_of_current_line def pytest_collection(self) -> None: @@ -761,9 +758,9 @@ def pytest_collection_finish(self, session: "Session") -> None: rep.toterminal(self._tw) def _printcollecteditems(self, items: Sequence[Item]) -> None: - # to print out items and their parent collectors + # To print out items and their parent collectors # we take care to leave out Instances aka () - # because later versions are going to get rid of them anyway + # because later versions are going to get rid of them anyway. if self.config.option.verbose < 0: if self.config.option.verbose < -1: counts = {} # type: Dict[str, int] @@ -868,7 +865,7 @@ def mkrel(nodeid): line += "[".join(values) return line - # collect_fspath comes from testid which has a "/"-normalized path + # collect_fspath comes from testid which has a "/"-normalized path. if fspath: res = mkrel(nodeid) @@ -896,7 +893,7 @@ def _getcrashline(self, rep): return "" # - # summaries for sessionfinish + # Summaries for sessionfinish. # def getreports(self, name: str): values = [] @@ -1255,9 +1252,9 @@ def _folded_skips( # For consistency, report all fspaths in relative form. fspath = startdir.bestrelpath(py.path.local(fspath)) keywords = getattr(event, "keywords", {}) - # folding reports with global pytestmark variable - # this is workaround, because for now we cannot identify the scope of a skip marker - # TODO: revisit after marks scope would be fixed + # Folding reports with global pytestmark variable. + # This is a workaround, because for now we cannot identify the scope of a skip marker + # TODO: Revisit after marks scope would be fixed. if ( event.when == "setup" and "skip" in keywords @@ -1298,20 +1295,19 @@ def _make_plural(count: int, noun: str) -> Tuple[int, str]: def _plugin_nameversions(plugininfo) -> List[str]: values = [] # type: List[str] for plugin, dist in plugininfo: - # gets us name and version! + # Gets us name and version! name = "{dist.project_name}-{dist.version}".format(dist=dist) - # questionable convenience, but it keeps things short + # Questionable convenience, but it keeps things short. if name.startswith("pytest-"): name = name[7:] - # we decided to print python package names - # they can have more than one plugin + # We decided to print python package names they can have more than one plugin. if name not in values: values.append(name) return values def format_session_duration(seconds: float) -> str: - """Format the given seconds in a human readable manner to show in the final summary""" + """Format the given seconds in a human readable manner to show in the final summary.""" if seconds < 60: return "{:.2f}s".format(seconds) else: diff --git a/src/_pytest/timing.py b/src/_pytest/timing.py index ded917b35bd..62442de7528 100644 --- a/src/_pytest/timing.py +++ b/src/_pytest/timing.py @@ -1,5 +1,4 @@ -""" -Indirection for time functions. +"""Indirection for time functions. We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect pytest runtime information (issue #185). diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 58dd659087d..017577a7ac8 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -1,4 +1,4 @@ -""" support for providing temporary directories to test functions. """ +"""Support for providing temporary directories to test functions.""" import os import re import tempfile @@ -22,13 +22,14 @@ class TempPathFactory: """Factory for temporary directories under the common base temp directory. - The base directory can be configured using the ``--basetemp`` option.""" + The base directory can be configured using the ``--basetemp`` option. + """ _given_basetemp = attr.ib( type=Path, - # using os.path.abspath() to get absolute path instead of resolve() as it - # does not work the same in all platforms (see #4427) - # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012) + # Use os.path.abspath() to get absolute path instead of resolve() as it + # does not work the same in all platforms (see #4427). + # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012). # Ignore type because of https://github.com/python/mypy/issues/6172. converter=attr.converters.optional( lambda p: Path(os.path.abspath(str(p))) # type: ignore @@ -38,10 +39,8 @@ class TempPathFactory: _basetemp = attr.ib(type=Optional[Path], default=None) @classmethod - def from_config(cls, config) -> "TempPathFactory": - """ - :param config: a pytest configuration - """ + def from_config(cls, config: Config) -> "TempPathFactory": + """Create a factory according to pytest configuration.""" return cls( given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir") ) @@ -55,7 +54,7 @@ def _ensure_relative_to_basetemp(self, basename: str) -> str: return basename def mktemp(self, basename: str, numbered: bool = True) -> Path: - """Creates a new temporary directory managed by the factory. + """Create a new temporary directory managed by the factory. :param basename: Directory base name, must be a relative path. @@ -66,7 +65,7 @@ def mktemp(self, basename: str, numbered: bool = True) -> Path: means that this function will create directories named ``"foo-0"``, ``"foo-1"``, ``"foo-2"`` and so on. - :return: + :returns: The path to the new directory. """ basename = self._ensure_relative_to_basetemp(basename) @@ -79,7 +78,7 @@ def mktemp(self, basename: str, numbered: bool = True) -> Path: return p def getbasetemp(self) -> Path: - """ return base temporary directory. """ + """Return base temporary directory.""" if self._basetemp is not None: return self._basetemp @@ -106,28 +105,23 @@ def getbasetemp(self) -> Path: @attr.s class TempdirFactory: - """ - backward comptibility wrapper that implements - :class:``py.path.local`` for :class:``TempPathFactory`` - """ + """Backward comptibility wrapper that implements :class:``py.path.local`` + for :class:``TempPathFactory``.""" _tmppath_factory = attr.ib(type=TempPathFactory) def mktemp(self, basename: str, numbered: bool = True) -> py.path.local: - """ - Same as :meth:`TempPathFactory.mkdir`, but returns a ``py.path.local`` object. - """ + """Same as :meth:`TempPathFactory.mkdir`, but returns a ``py.path.local`` object.""" return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve()) def getbasetemp(self) -> py.path.local: - """backward compat wrapper for ``_tmppath_factory.getbasetemp``""" + """Backward compat wrapper for ``_tmppath_factory.getbasetemp``.""" return py.path.local(self._tmppath_factory.getbasetemp().resolve()) def get_user() -> Optional[str]: """Return the current user name, or None if getuser() does not work - in the current environment (see #1010). - """ + in the current environment (see #1010).""" import getpass try: @@ -153,16 +147,14 @@ def pytest_configure(config: Config) -> None: @pytest.fixture(scope="session") def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: - """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session. - """ + """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.""" # Set dynamically by pytest_configure() above. return request.config._tmpdirhandler # type: ignore @pytest.fixture(scope="session") def tmp_path_factory(request: FixtureRequest) -> TempPathFactory: - """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session. - """ + """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.""" # Set dynamically by pytest_configure() above. return request.config._tmp_path_factory # type: ignore @@ -177,11 +169,11 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: @pytest.fixture def tmpdir(tmp_path: Path) -> py.path.local: - """Return a temporary directory path object - which is unique to each test function invocation, - created as a sub directory of the base temporary - directory. The returned object is a `py.path.local`_ - path object. + """Return a temporary directory path object which is unique to each test + function invocation, created as a sub directory of the base temporary + directory. + + The returned object is a `py.path.local`_ path object. .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html """ @@ -190,15 +182,15 @@ def tmpdir(tmp_path: Path) -> py.path.local: @pytest.fixture def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path: - """Return a temporary directory path object - which is unique to each test function invocation, - created as a sub directory of the base temporary - directory. The returned object is a :class:`pathlib.Path` - object. + """Return a temporary directory path object which is unique to each test + function invocation, created as a sub directory of the base temporary + directory. + + The returned object is a :class:`pathlib.Path` object. .. note:: - in python < 3.6 this is a pathlib2.Path + In python < 3.6 this is a pathlib2.Path. """ return _mk_tmp(request, tmp_path_factory) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 782a5c36962..75c53a55277 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -1,4 +1,4 @@ -""" discovery and running of std-library "unittest" style tests. """ +"""Discover and run std-library "unittest" style tests.""" import sys import traceback import types @@ -46,7 +46,7 @@ def pytest_pycollect_makeitem( collector: PyCollector, name: str, obj: object ) -> Optional["UnitTestCase"]: - # has unittest been imported and is obj a subclass of its TestCase? + # Has unittest been imported and is obj a subclass of its TestCase? try: ut = sys.modules["unittest"] # Type ignored because `ut` is an opaque module. @@ -54,14 +54,14 @@ def pytest_pycollect_makeitem( return None except Exception: return None - # yes, so let's collect it + # Yes, so let's collect it. item = UnitTestCase.from_parent(collector, name=name, obj=obj) # type: UnitTestCase return item class UnitTestCase(Class): - # marker for fixturemanger.getfixtureinfo() - # to declare that our children do not support funcargs + # Marker for fixturemanger.getfixtureinfo() + # to declare that our children do not support funcargs. nofuncargs = True def collect(self) -> Iterable[Union[Item, Collector]]: @@ -97,7 +97,7 @@ def collect(self) -> Iterable[Union[Item, Collector]]: def _inject_setup_teardown_fixtures(self, cls: type) -> None: """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding - teardown functions (#517)""" + teardown functions (#517).""" class_fixture = _make_xunit_fixture( cls, "setUpClass", "tearDownClass", scope="class", pass_self=False ) @@ -145,7 +145,7 @@ class TestCaseFunction(Function): _testcase = None # type: Optional[unittest.TestCase] def setup(self) -> None: - # a bound method to be called during teardown() if set (see 'runtest()') + # A bound method to be called during teardown() if set (see 'runtest()'). self._explicit_tearDown = None # type: Optional[Callable[[], None]] assert self.parent is not None self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined] @@ -164,12 +164,12 @@ def startTest(self, testcase: "unittest.TestCase") -> None: pass def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None: - # unwrap potential exception info (see twisted trial support below) + # Unwrap potential exception info (see twisted trial support below). rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) try: excinfo = _pytest._code.ExceptionInfo(rawexcinfo) # type: ignore[arg-type] - # invoke the attributes to trigger storing the traceback - # trial causes some issue there + # Invoke the attributes to trigger storing the traceback + # trial causes some issue there. excinfo.value excinfo.traceback except TypeError: @@ -242,7 +242,7 @@ def stopTest(self, testcase: "unittest.TestCase") -> None: def _expecting_failure(self, test_method) -> bool: """Return True if the given unittest method (or the entire class) is marked - with @expectedFailure""" + with @expectedFailure.""" expecting_failure_method = getattr( test_method, "__unittest_expecting_failure__", False ) @@ -256,23 +256,23 @@ def runtest(self) -> None: maybe_wrap_pytest_function_for_tracing(self) - # let the unittest framework handle async functions + # Let the unittest framework handle async functions. if is_async_function(self.obj): # Type ignored because self acts as the TestResult, but is not actually one. self._testcase(result=self) # type: ignore[arg-type] else: - # when --pdb is given, we want to postpone calling tearDown() otherwise + # When --pdb is given, we want to postpone calling tearDown() otherwise # when entering the pdb prompt, tearDown() would have probably cleaned up - # instance variables, which makes it difficult to debug - # arguably we could always postpone tearDown(), but this changes the moment where the + # instance variables, which makes it difficult to debug. + # Arguably we could always postpone tearDown(), but this changes the moment where the # TestCase instance interacts with the results object, so better to only do it - # when absolutely needed + # when absolutely needed. if self.config.getoption("usepdb") and not _is_skipped(self.obj): self._explicit_tearDown = self._testcase.tearDown setattr(self._testcase, "tearDown", lambda *args: None) - # we need to update the actual bound method with self.obj, because - # wrap_pytest_function_for_tracing replaces self.obj by a wrapper + # We need to update the actual bound method with self.obj, because + # wrap_pytest_function_for_tracing replaces self.obj by a wrapper. setattr(self._testcase, self.name, self.obj) try: self._testcase(result=self) # type: ignore[arg-type] @@ -305,14 +305,14 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined] ): excinfo = call.excinfo - # let's substitute the excinfo with a pytest.skip one + # Let's substitute the excinfo with a pytest.skip one. call2 = CallInfo[None].from_call( lambda: pytest.skip(str(excinfo.value)), call.when ) call.excinfo = call2.excinfo -# twisted trial support +# Twisted trial support. @hookimpl(hookwrapper=True) @@ -356,5 +356,5 @@ def check_testcase_implements_trial_reporter(done: List[int] = []) -> None: def _is_skipped(obj) -> bool: - """Return True if the given object has been marked with @unittest.skip""" + """Return True if the given object has been marked with @unittest.skip.""" return bool(getattr(obj, "__unittest_skip__", False)) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 6f3b88da8b8..c93b9604907 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -99,7 +99,7 @@ class UnformattedWarning(Generic[_W]): template = attr.ib(type=str) def format(self, **kwargs: Any) -> _W: - """Returns an instance of the warning category, formatted with given kwargs""" + """Return an instance of the warning category, formatted with given kwargs.""" return self.category(self.template.format(**kwargs)) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 3a8f2d8b33f..0604aa60b18 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -87,8 +87,7 @@ def catch_warnings_for_item( when: "Literal['config', 'collect', 'runtest']", item: Optional[Item], ) -> Generator[None, None, None]: - """ - Context manager that catches warnings generated in the contained execution block. + """Context manager that catches warnings generated in the contained execution block. ``item`` can be None if we are not in the context of an item execution. @@ -101,14 +100,14 @@ def catch_warnings_for_item( assert log is not None if not sys.warnoptions: - # if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908) + # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908). warnings.filterwarnings("always", category=DeprecationWarning) warnings.filterwarnings("always", category=PendingDeprecationWarning) warnings.filterwarnings("error", category=pytest.PytestDeprecationWarning) - # filters should have this precedence: mark, cmdline options, ini - # filters should be applied in the inverse order of precedence + # Filters should have this precedence: mark, cmdline options, ini. + # Filters should be applied in the inverse order of precedence. for arg in inifilters: warnings.filterwarnings(*_parse_filter(arg, escape=False)) @@ -193,14 +192,16 @@ def pytest_sessionfinish(session: Session) -> Generator[None, None, None]: def _issue_warning_captured(warning: Warning, hook, stacklevel: int) -> None: - """ - This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: - at this point the actual options might not have been set, so we manually trigger the pytest_warning_recorded - hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891. + """A function that should be used instead of calling ``warnings.warn`` + directly when we are in the "configure" stage. + + At this point the actual options might not have been set, so we manually + trigger the pytest_warning_recorded hook so we can display these warnings + in the terminal. This is a hack until we can sort out #2891. - :param warning: the warning instance. - :param hook: the hook caller - :param stacklevel: stacklevel forwarded to warnings.warn + :param warning: The warning instance. + :param hook: The hook caller. + :param stacklevel: stacklevel forwarded to warnings.warn. """ with warnings.catch_warnings(record=True) as records: warnings.simplefilter("always", type(warning)) diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 64d6d1f23ee..c4c28191877 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -1,7 +1,5 @@ # PYTHON_ARGCOMPLETE_OK -""" -pytest: unit and functional testing with Python. -""" +"""pytest: unit and functional testing with Python.""" from . import collect from _pytest import __version__ from _pytest.assertion import register_assert_rewrite diff --git a/src/pytest/__main__.py b/src/pytest/__main__.py index 25b1e45b89d..b170152937b 100644 --- a/src/pytest/__main__.py +++ b/src/pytest/__main__.py @@ -1,6 +1,4 @@ -""" -pytest entry point -""" +"""The pytest entry point.""" import pytest if __name__ == "__main__": diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 26a34c6565d..cc03385f3a9 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -2,12 +2,14 @@ import stat import sys from typing import Callable +from typing import cast from typing import List import attr import pytest from _pytest import pathlib +from _pytest.config import Config from _pytest.pathlib import cleanup_numbered_dir from _pytest.pathlib import create_cleanup_lock from _pytest.pathlib import make_numbered_dir @@ -45,7 +47,7 @@ def option(self): class TestTempdirHandler: def test_mktemp(self, tmp_path): - config = FakeConfig(tmp_path) + config = cast(Config, FakeConfig(tmp_path)) t = TempdirFactory(TempPathFactory.from_config(config)) tmp = t.mktemp("world") assert tmp.relto(t.getbasetemp()) == "world0" @@ -58,7 +60,7 @@ def test_mktemp(self, tmp_path): def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch): """#4425""" monkeypatch.chdir(tmp_path) - config = FakeConfig("hello") + config = cast(Config, FakeConfig("hello")) t = TempPathFactory.from_config(config) assert t.getbasetemp().resolve() == (tmp_path / "hello").resolve() From cbec0f8c6ad01a3987506c27d2c0d0706d479659 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 17 Jul 2020 23:54:41 +0300 Subject: [PATCH 532/823] CONTRIBUTING: document the docstring style we use --- CONTRIBUTING.rst | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 0523c0ece77..4481fee3818 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -89,6 +89,38 @@ without using a local copy. This can be convenient for small fixes. The built documentation should be available in ``doc/en/_build/html``, where 'en' refers to the documentation language. +Pytest has an API reference which in large part is +`generated automatically `_ +from the docstrings of the documented items. Pytest uses the +`Sphinx docstring format `_. +For example: + +.. code-block:: python + + def my_function(arg: ArgType) -> Foo: + """Do important stuff. + + More detailed info here, in separate paragraphs from the subject line. + Use proper sentences -- start sentences with capital letters and end + with periods. + + Can include annotated documentation: + + :param short_arg: An argument which determines stuff. + :param long_arg: + A long explanation which spans multiple lines, overflows + like this. + :returns: The result. + :raises ValueError: + Detailed information when this can happen. + + .. versionadded:: 6.0 + + Including types into the annotations above is not necessary when + type-hinting is being used (as in this example). + """ + + .. _submitplugin: Submitting Plugins to pytest-dev From a2d562d369f0b826b555755be57cb6ee7c73ed30 Mon Sep 17 00:00:00 2001 From: "C. Titus Brown" Date: Sat, 1 Aug 2020 07:33:03 -0700 Subject: [PATCH 533/823] Minor formatting fix in xunit_setup.rst Fixed location of double-backquote for verbatim text. --- doc/en/xunit_setup.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/xunit_setup.rst b/doc/en/xunit_setup.rst index 83545223ae3..8b3366f62ae 100644 --- a/doc/en/xunit_setup.rst +++ b/doc/en/xunit_setup.rst @@ -12,7 +12,7 @@ fixtures (setup and teardown test state) on a per-module/class/function basis. .. note:: While these setup/teardown methods are simple and familiar to those - coming from a ``unittest`` or nose ``background``, you may also consider + coming from a ``unittest`` or ``nose`` background, you may also consider using pytest's more powerful :ref:`fixture mechanism ` which leverages the concept of dependency injection, allowing for a more modular and more scalable approach for managing test state, From d7ad55bb2a56f13dbf0580dabae25efd985254f4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 1 Aug 2020 13:46:35 -0300 Subject: [PATCH 534/823] Add docs for releasing major/release candidates Fix #7447 --- RELEASING.rst | 75 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/RELEASING.rst b/RELEASING.rst index 5893987e37f..984368618b1 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -5,37 +5,82 @@ Our current policy for releasing is to aim for a bug-fix release every few weeks is to get fixes and new features out instead of trying to cram a ton of features into a release and by consequence taking a lot of time to make a new one. +The git commands assume the following remotes are setup: + +* ``origin``: your own fork of the repository. +* ``upstream``: the ``pytest-dev/pytest`` official repository. + Preparing: Automatic Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~ We have developed an automated workflow for releases, that uses GitHub workflows and is triggered -by opening an issue or issuing a comment one. +by opening an issue. + +Bug-fix releases +^^^^^^^^^^^^^^^^ + +A bug-fix release is always done from a maintenance branch, so for example to release bug-fix +``5.1.2``, open a new issue and add this comment to the body:: + + @pytestbot please prepare release from 5.1.x + +Where ``5.1.x`` is the maintenance branch for the ``5.1`` series. + +The automated workflow will publish a PR and notify it as a comment in the issue. + +Minor releases +^^^^^^^^^^^^^^ + +1. Create a new maintenance branch from ``master``:: + + git fetch --all + git branch 5.2.x upstream/master + git push upstream 5.2.x -The comment must be in the form:: +2. Open a new issue and add this comment to the body:: - @pytestbot please prepare release from BRANCH + @pytestbot please prepare release from 5.2.x -Where ``BRANCH`` is ``master`` or one of the maintenance branches. +The automated workflow will publish a PR and notify it as a comment in the issue. -For major releases the comment must be in the form:: +Major and release candidates +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - @pytestbot please prepare major release from master +1. Create a new maintenance branch from ``master``:: -After that, the workflow should publish a PR and notify that it has done so as a comment -in the original issue. + git fetch --all + git branch 6.0.x upstream/master + git push upstream 6.0.x + +2. For a **major release**, open a new issue and add this comment in the body:: + + @pytestbot please prepare major release from 6.0.x + + For a **release candidate**, the comment must be (TODO: `#7551 `__):: + + @pytestbot please prepare release candidate from 6.0.x + +The automated workflow will publish a PR and notify it as a comment in the issue. + +At this point on, this follows the same workflow as other maintenance branches: bug-fixes are merged +into ``master`` and ported back to the maintenance branch, even for release candidates. + +**A note about release candidates** + +During release candidates we can merge small improvements into +the maintenance branch before releasing the final major version, however we must take care +to avoid introducing big changes at this stage. Preparing: Manual Method ~~~~~~~~~~~~~~~~~~~~~~~~ -.. important:: - - pytest releases must be prepared on **Linux** because the docs and examples expect - to be executed on that platform. +**Important**: pytest releases must be prepared on **Linux** because the docs and examples expect +to be executed on that platform. To release a version ``MAJOR.MINOR.PATCH``, follow these steps: -#. For major and minor releases, create a new branch ``MAJOR.MINOR.x`` from the - latest ``master`` and push it to the ``pytest-dev/pytest`` repo. +#. For major and minor releases, create a new branch ``MAJOR.MINOR.x`` from + ``upstream/master`` and push it to ``upstream``. #. Create a branch ``release-MAJOR.MINOR.PATCH`` from the ``MAJOR.MINOR.x`` branch. @@ -69,7 +114,7 @@ Both automatic and manual processes described above follow the same steps from t git fetch --all --prune git checkout origin/master -b cherry-pick-release - git cherry-pick -x -m1 origin/MAJOR.MINOR.x + git cherry-pick -x -m1 upstream/MAJOR.MINOR.x #. Open a PR for ``cherry-pick-release`` and merge it once CI passes. No need to wait for approvals if there were no conflicts on the previous step. From d1fa749b834013904514f5af235d0d562d96c671 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 1 Aug 2020 14:21:36 -0300 Subject: [PATCH 535/823] Add readthedocs config file to use pip for installation --- .readthedocs.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000000..0176c264001 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,12 @@ +version: 2 + +python: + version: 3.7 + install: + - requirements: doc/en/requirements.txt + - method: pip + path: . + +formats: + - epub + - pdf From be656dd4e4444acd9600660fc3b6bf3896067dbe Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 1 Aug 2020 13:06:13 +0300 Subject: [PATCH 536/823] typing: set disallow_any_generics This prevents referring to a generic type without filling in its generic type parameters. The FixtureDef typing might need some more refining in the future. --- setup.cfg | 1 + src/_pytest/_code/code.py | 23 +++++++++----- src/_pytest/assertion/rewrite.py | 2 +- src/_pytest/cacheprovider.py | 2 +- src/_pytest/debugging.py | 7 +++-- src/_pytest/fixtures.py | 40 ++++++++++++------------ src/_pytest/hookspec.py | 6 ++-- src/_pytest/main.py | 2 +- src/_pytest/mark/structures.py | 18 ++++++----- src/_pytest/nodes.py | 7 +++-- src/_pytest/outcomes.py | 2 +- src/_pytest/python.py | 4 +-- src/_pytest/python_api.py | 8 ++--- src/_pytest/recwarn.py | 10 +++--- src/_pytest/reports.py | 2 +- src/_pytest/runner.py | 18 +++++------ src/_pytest/setuponly.py | 6 ++-- src/_pytest/setupplan.py | 2 +- src/_pytest/terminal.py | 4 +-- src/_pytest/unittest.py | 6 ++-- testing/python/metafunc.py | 2 +- testing/test_assertion.py | 3 +- testing/test_terminal.py | 52 ++++++++++++++++---------------- 23 files changed, 123 insertions(+), 104 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4ba47dc8068..9a4d841e9a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -96,6 +96,7 @@ formats = sdist.tgz,bdist_wheel [mypy] mypy_path = src check_untyped_defs = True +disallow_any_generics = True ignore_missing_imports = True no_implicit_optional = True show_error_codes = True diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index e0aadd724ca..4461fbfc99e 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -613,7 +613,7 @@ def getrepr( ) return fmt.repr_excinfo(self) - def match(self, regexp: "Union[str, Pattern]") -> "Literal[True]": + def match(self, regexp: "Union[str, Pattern[str]]") -> "Literal[True]": """Check whether the regular expression `regexp` matches the string representation of the exception using :func:`python:re.search`. @@ -678,7 +678,7 @@ def get_source( self, source: "Source", line_index: int = -1, - excinfo: Optional[ExceptionInfo] = None, + excinfo: Optional[ExceptionInfo[BaseException]] = None, short: bool = False, ) -> List[str]: """Return formatted and marked up source lines.""" @@ -703,7 +703,10 @@ def get_source( return lines def get_exconly( - self, excinfo: ExceptionInfo, indent: int = 4, markall: bool = False + self, + excinfo: ExceptionInfo[BaseException], + indent: int = 4, + markall: bool = False, ) -> List[str]: lines = [] indentstr = " " * indent @@ -743,7 +746,9 @@ def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]: return None def repr_traceback_entry( - self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None + self, + entry: TracebackEntry, + excinfo: Optional[ExceptionInfo[BaseException]] = None, ) -> "ReprEntry": lines = [] # type: List[str] style = entry._repr_style if entry._repr_style is not None else self.style @@ -785,7 +790,7 @@ def _makepath(self, path): path = np return path - def repr_traceback(self, excinfo: ExceptionInfo) -> "ReprTraceback": + def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": traceback = excinfo.traceback if self.tbfilter: traceback = traceback.filter() @@ -850,12 +855,14 @@ def _truncate_recursive_traceback( return traceback, extraline - def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr": + def repr_excinfo( + self, excinfo: ExceptionInfo[BaseException] + ) -> "ExceptionChainRepr": repr_chain = ( [] ) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]] - e = excinfo.value - excinfo_ = excinfo # type: Optional[ExceptionInfo] + e = excinfo.value # type: Optional[BaseException] + excinfo_ = excinfo # type: Optional[ExceptionInfo[BaseException]] descr = None seen = set() # type: Set[int] while e is not None and id(e) not in seen: diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 730d5382ad2..ec3669a2e52 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -710,7 +710,7 @@ def run(self, mod: ast.Module) -> None: node = nodes.pop() for name, field in ast.iter_fields(node): if isinstance(field, list): - new = [] # type: List + new = [] # type: List[ast.AST] for i, child in enumerate(field): if isinstance(child, ast.Assert): # Transform assert. diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 41c2582712c..bc26c26bcd6 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -181,7 +181,7 @@ def __init__(self, lfplugin: "LFPlugin") -> None: self._collected_at_least_one_failure = False @pytest.hookimpl(hookwrapper=True) - def pytest_make_collect_report(self, collector: nodes.Collector) -> Generator: + def pytest_make_collect_report(self, collector: nodes.Collector): if isinstance(collector, Session): out = yield res = out.get_result() # type: CollectReport diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 5dda4b8d71c..69e6b4dd4a6 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -3,7 +3,10 @@ import functools import sys import types +from typing import Any +from typing import Callable from typing import Generator +from typing import List from typing import Tuple from typing import Union @@ -91,7 +94,7 @@ class pytestPDB: _pluginmanager = None # type: PytestPluginManager _config = None # type: Config - _saved = [] # type: list + _saved = [] # type: List[Tuple[Callable[..., None], PytestPluginManager, Config]] _recursive_debug = 0 _wrapped_pdb_cls = None @@ -274,7 +277,7 @@ def set_trace(cls, *args, **kwargs) -> None: class PdbInvoke: def pytest_exception_interact( - self, node: Node, call: "CallInfo", report: BaseReport + self, node: Node, call: "CallInfo[Any]", report: BaseReport ) -> None: capman = node.config.pluginmanager.getplugin("capturemanager") if capman: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 5dbaf9e0696..6510985219a 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -94,8 +94,8 @@ @attr.s(frozen=True) -class PseudoFixtureDef: - cached_result = attr.ib(type="_FixtureCachedResult") +class PseudoFixtureDef(Generic[_FixtureValue]): + cached_result = attr.ib(type="_FixtureCachedResult[_FixtureValue]") scope = attr.ib(type="_Scope") @@ -141,7 +141,7 @@ def provide(self): return decoratescope -def get_scope_package(node, fixturedef: "FixtureDef"): +def get_scope_package(node, fixturedef: "FixtureDef[object]"): import pytest cls = pytest.Package @@ -397,7 +397,7 @@ class FuncFixtureInfo: # definitions. initialnames = attr.ib(type=Tuple[str, ...]) names_closure = attr.ib(type=List[str]) - name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef"]]) + name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef[Any]"]]) def prune_dependency_tree(self) -> None: """Recompute names_closure from initialnames and name2fixturedefs. @@ -441,7 +441,7 @@ def __init__(self, pyfuncitem) -> None: self.fixturename = None # type: Optional[str] #: Scope string, one of "function", "class", "module", "session". self.scope = "function" # type: _Scope - self._fixture_defs = {} # type: Dict[str, FixtureDef] + self._fixture_defs = {} # type: Dict[str, FixtureDef[Any]] fixtureinfo = pyfuncitem._fixtureinfo # type: FuncFixtureInfo self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2index = {} # type: Dict[str, int] @@ -467,7 +467,7 @@ def node(self): """Underlying collection node (depends on current request scope).""" return self._getscopeitem(self.scope) - def _getnextfixturedef(self, argname: str) -> "FixtureDef": + def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]": fixturedefs = self._arg2fixturedefs.get(argname, None) if fixturedefs is None: # We arrive here because of a dynamic call to @@ -586,7 +586,7 @@ def getfixturevalue(self, argname: str) -> Any: def _get_active_fixturedef( self, argname: str - ) -> Union["FixtureDef", PseudoFixtureDef]: + ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]: try: return self._fixture_defs[argname] except KeyError: @@ -604,9 +604,9 @@ def _get_active_fixturedef( self._fixture_defs[argname] = fixturedef return fixturedef - def _get_fixturestack(self) -> List["FixtureDef"]: + def _get_fixturestack(self) -> List["FixtureDef[Any]"]: current = self - values = [] # type: List[FixtureDef] + values = [] # type: List[FixtureDef[Any]] while 1: fixturedef = getattr(current, "_fixturedef", None) if fixturedef is None: @@ -616,7 +616,7 @@ def _get_fixturestack(self) -> List["FixtureDef"]: assert isinstance(current, SubRequest) current = current._parent_request - def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: + def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: """Create a SubRequest based on "self" and call the execute method of the given FixtureDef object. @@ -689,7 +689,7 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None: self._schedule_finalizers(fixturedef, subrequest) def _schedule_finalizers( - self, fixturedef: "FixtureDef", subrequest: "SubRequest" + self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" ) -> None: # If fixture function failed it might have registered finalizers. self.session._setupstate.addfinalizer( @@ -751,7 +751,7 @@ def __init__( scope: "_Scope", param, param_index: int, - fixturedef: "FixtureDef", + fixturedef: "FixtureDef[object]", ) -> None: self._parent_request = request self.fixturename = fixturedef.argname @@ -773,7 +773,7 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._fixturedef.addfinalizer(finalizer) def _schedule_finalizers( - self, fixturedef: "FixtureDef", subrequest: "SubRequest" + self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" ) -> None: # If the executing fixturedef was not explicitly requested in the argument list (via # getfixturevalue inside the fixture call) then ensure this fixture def will be finished @@ -1456,8 +1456,8 @@ class FixtureManager: def __init__(self, session: "Session") -> None: self.session = session self.config = session.config # type: Config - self._arg2fixturedefs = {} # type: Dict[str, List[FixtureDef]] - self._holderobjseen = set() # type: Set + self._arg2fixturedefs = {} # type: Dict[str, List[FixtureDef[Any]]] + self._holderobjseen = set() # type: Set[object] self._nodeid_and_autousenames = [ ("", self.config.getini("usefixtures")) ] # type: List[Tuple[str, List[str]]] @@ -1534,7 +1534,7 @@ def _getautousenames(self, nodeid: str) -> List[str]: def getfixtureclosure( self, fixturenames: Tuple[str, ...], parentnode, ignore_args: Sequence[str] = () - ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef]]]: + ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]: # Collect the closure of all fixtures, starting with the given # fixturenames as the initial set. As we have to visit all # factory definitions anyway, we also return an arg2fixturedefs @@ -1557,7 +1557,7 @@ def merge(otherlist: Iterable[str]) -> None: # need to return it as well, so save this. initialnames = tuple(fixturenames_closure) - arg2fixturedefs = {} # type: Dict[str, Sequence[FixtureDef]] + arg2fixturedefs = {} # type: Dict[str, Sequence[FixtureDef[Any]]] lastlen = -1 while lastlen != len(fixturenames_closure): lastlen = len(fixturenames_closure) @@ -1677,7 +1677,7 @@ def parsefactories( def getfixturedefs( self, argname: str, nodeid: str - ) -> Optional[Sequence[FixtureDef]]: + ) -> Optional[Sequence[FixtureDef[Any]]]: """Get a list of fixtures which are applicable to the given node id. :param str argname: Name of the fixture to search for. @@ -1691,8 +1691,8 @@ def getfixturedefs( return tuple(self._matchfactories(fixturedefs, nodeid)) def _matchfactories( - self, fixturedefs: Iterable[FixtureDef], nodeid: str - ) -> Iterator[FixtureDef]: + self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str + ) -> Iterator[FixtureDef[Any]]: from _pytest import nodes for fixturedef in fixturedefs: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 60b1b643aed..ce435901c44 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -533,7 +533,7 @@ def pytest_report_from_serializable( @hookspec(firstresult=True) def pytest_fixture_setup( - fixturedef: "FixtureDef", request: "SubRequest" + fixturedef: "FixtureDef[Any]", request: "SubRequest" ) -> Optional[object]: """Perform fixture setup execution. @@ -549,7 +549,7 @@ def pytest_fixture_setup( def pytest_fixture_post_finalizer( - fixturedef: "FixtureDef", request: "SubRequest" + fixturedef: "FixtureDef[Any]", request: "SubRequest" ) -> None: """Called after fixture teardown, but before the cache is cleared, so the fixture result ``fixturedef.cached_result`` is still available (not @@ -826,7 +826,7 @@ def pytest_keyboard_interrupt( def pytest_exception_interact( node: Union["Item", "Collector"], - call: "CallInfo[object]", + call: "CallInfo[Any]", report: Union["CollectReport", "TestReport"], ) -> None: """Called when an exception was raised which can potentially be diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 292ba58e24b..c0e1c9d076e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -404,7 +404,7 @@ class Failed(Exception): @attr.s -class _bestrelpath_cache(dict): +class _bestrelpath_cache(Dict[py.path.local, str]): path = attr.ib(type=py.path.local) def __missing__(self, path: py.path.local) -> str: diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 5abe4b94532..ea1ba546c3f 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -5,6 +5,7 @@ from typing import Any from typing import Callable from typing import Iterable +from typing import Iterator from typing import List from typing import Mapping from typing import NamedTuple @@ -30,6 +31,8 @@ if TYPE_CHECKING: from typing import Type + from ..nodes import Node + EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" @@ -521,13 +524,14 @@ def __getattr__(self, name: str) -> MarkDecorator: MARK_GEN = MarkGenerator() -class NodeKeywords(collections.abc.MutableMapping): - def __init__(self, node): +# TODO(py36): inherit from typing.MutableMapping[str, Any]. +class NodeKeywords(collections.abc.MutableMapping): # type: ignore[type-arg] + def __init__(self, node: "Node") -> None: self.node = node self.parent = node.parent self._markers = {node.name: True} - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: try: return self._markers[key] except KeyError: @@ -535,17 +539,17 @@ def __getitem__(self, key): raise return self.parent.keywords[key] - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: self._markers[key] = value - def __delitem__(self, key): + def __delitem__(self, key: str) -> None: raise ValueError("cannot delete key in keywords dict") - def __iter__(self): + def __iter__(self) -> Iterator[str]: seen = self._seen() return iter(seen) - def _seen(self): + def _seen(self) -> Set[str]: seen = set(self._markers) if self.parent is not None: seen.update(self.parent.keywords) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index cc1cc7ebdd0..9522f418472 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,6 +1,7 @@ import os import warnings from functools import lru_cache +from typing import Any from typing import Callable from typing import Dict from typing import Iterable @@ -167,7 +168,7 @@ def __init__( self.extra_keyword_matches = set() # type: Set[str] # Used for storing artificial fixturedefs for direct parametrization. - self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef] + self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef[Any]] if nodeid is not None: assert "::()" not in nodeid @@ -354,7 +355,7 @@ def getparent(self, cls: "Type[_NodeType]") -> Optional[_NodeType]: assert current is None or isinstance(current, cls) return current - def _prunetraceback(self, excinfo): + def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: pass def _repr_failure_py( @@ -479,7 +480,7 @@ def repr_failure( # type: ignore[override] return self._repr_failure_py(excinfo, style=tbstyle) - def _prunetraceback(self, excinfo): + def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: if hasattr(self, "fspath"): traceback = excinfo.traceback ntraceback = traceback.cut(path=self.fspath) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index f083689edba..3ce026b89b3 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -83,7 +83,7 @@ def __init__( # Elaborate hack to work around https://github.com/python/mypy/issues/2087. # Ideally would just be `exit.Exception = Exit` etc. -_F = TypeVar("_F", bound=Callable) +_F = TypeVar("_F", bound=Callable[..., object]) _ET = TypeVar("_ET", bound="Type[BaseException]") diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 589dfd06e27..d942b4fa690 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1179,7 +1179,7 @@ def _validate_if_using_arg_names( def _find_parametrized_scope( argnames: typing.Sequence[str], - arg2fixturedefs: Mapping[str, typing.Sequence[fixtures.FixtureDef]], + arg2fixturedefs: Mapping[str, typing.Sequence[fixtures.FixtureDef[object]]], indirect: Union[bool, typing.Sequence[str]], ) -> "fixtures._Scope": """Find the most appropriate scope for a parametrized call based on its arguments. @@ -1578,7 +1578,7 @@ def setup(self) -> None: self.obj = self._getobj() self._request._fillfixtures() - def _prunetraceback(self, excinfo: ExceptionInfo) -> None: + def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): code = _pytest._code.Code(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 1bad5c777b6..b003db72abd 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -526,7 +526,7 @@ def _is_numpy_array(obj: object) -> bool: def raises( expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], *, - match: "Optional[Union[str, Pattern]]" = ... + match: "Optional[Union[str, Pattern[str]]]" = ... ) -> "RaisesContext[_E]": ... # pragma: no cover @@ -534,7 +534,7 @@ def raises( @overload # noqa: F811 def raises( # noqa: F811 expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], - func: Callable, + func: Callable[..., Any], *args: Any, **kwargs: Any ) -> _pytest._code.ExceptionInfo[_E]: @@ -670,7 +670,7 @@ def raises( # noqa: F811 message = "DID NOT RAISE {}".format(expected_exception) if not args: - match = kwargs.pop("match", None) + match = kwargs.pop("match", None) # type: Optional[Union[str, Pattern[str]]] if kwargs: msg = "Unexpected keyword arguments passed to pytest.raises: " msg += ", ".join(sorted(kwargs)) @@ -703,7 +703,7 @@ def __init__( self, expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], message: str, - match_expr: Optional[Union[str, "Pattern"]] = None, + match_expr: Optional[Union[str, "Pattern[str]"]] = None, ) -> None: self.expected_exception = expected_exception self.message = message diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index ded414ab466..7eb7020d02f 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -40,7 +40,7 @@ def recwarn() -> Generator["WarningsRecorder", None, None]: @overload def deprecated_call( - *, match: Optional[Union[str, "Pattern"]] = ... + *, match: Optional[Union[str, "Pattern[str]"]] = ... ) -> "WarningsRecorder": raise NotImplementedError() @@ -53,7 +53,7 @@ def deprecated_call( # noqa: F811 def deprecated_call( # noqa: F811 - func: Optional[Callable] = None, *args: Any, **kwargs: Any + func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any ) -> Union["WarningsRecorder", Any]: """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning``. @@ -87,7 +87,7 @@ def deprecated_call( # noqa: F811 def warns( expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]], *, - match: "Optional[Union[str, Pattern]]" = ... + match: "Optional[Union[str, Pattern[str]]]" = ... ) -> "WarningsChecker": raise NotImplementedError() @@ -105,7 +105,7 @@ def warns( # noqa: F811 def warns( # noqa: F811 expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]], *args: Any, - match: Optional[Union[str, "Pattern"]] = None, + match: Optional[Union[str, "Pattern[str]"]] = None, **kwargs: Any ) -> Union["WarningsChecker", Any]: r"""Assert that code raises a particular class of warning. @@ -234,7 +234,7 @@ def __init__( expected_warning: Optional[ Union["Type[Warning]", Tuple["Type[Warning]", ...]] ] = None, - match_expr: Optional[Union[str, "Pattern"]] = None, + match_expr: Optional[Union[str, "Pattern[str]"]] = None, ) -> None: super().__init__() diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 65098343bf2..8461ad663e6 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -514,7 +514,7 @@ def deserialize_repr_traceback(repr_traceback_dict): ] return ReprTraceback(**repr_traceback_dict) - def deserialize_repr_crash(repr_crash_dict: Optional[dict]): + def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]): if repr_crash_dict is not None: return ReprFileLocation(**repr_crash_dict) else: diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 289d676d607..4923406b9ba 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -213,7 +213,7 @@ def call_and_report( return report -def check_interactive_exception(call: "CallInfo", report: BaseReport) -> bool: +def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool: """Check whether the call raised an exception that should be reported as interactive.""" if call.excinfo is None: @@ -247,11 +247,11 @@ def call_runtest_hook( ) -_T = TypeVar("_T") +TResult = TypeVar("TResult", covariant=True) @attr.s(repr=False) -class CallInfo(Generic[_T]): +class CallInfo(Generic[TResult]): """Result/Exception info a function invocation. :param T result: @@ -269,7 +269,7 @@ class CallInfo(Generic[_T]): The context of invocation: "setup", "call", "teardown", ... """ - _result = attr.ib(type="Optional[_T]") + _result = attr.ib(type="Optional[TResult]") excinfo = attr.ib(type=Optional[ExceptionInfo[BaseException]]) start = attr.ib(type=float) stop = attr.ib(type=float) @@ -277,26 +277,26 @@ class CallInfo(Generic[_T]): when = attr.ib(type="Literal['collect', 'setup', 'call', 'teardown']") @property - def result(self) -> _T: + def result(self) -> TResult: if self.excinfo is not None: raise AttributeError("{!r} has no valid result".format(self)) # The cast is safe because an exception wasn't raised, hence # _result has the expected function return type (which may be # None, that's why a cast and not an assert). - return cast(_T, self._result) + return cast(TResult, self._result) @classmethod def from_call( cls, - func: "Callable[[], _T]", + func: "Callable[[], TResult]", when: "Literal['collect', 'setup', 'call', 'teardown']", reraise: "Optional[Union[Type[BaseException], Tuple[Type[BaseException], ...]]]" = None, - ) -> "CallInfo[_T]": + ) -> "CallInfo[TResult]": excinfo = None start = timing.time() precise_start = timing.perf_counter() try: - result = func() # type: Optional[_T] + result = func() # type: Optional[TResult] except BaseException: excinfo = ExceptionInfo.from_current() if reraise is not None and isinstance(excinfo.value, reraise): diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index dfd01cc76b2..44a1094c0d2 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -29,7 +29,7 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(hookwrapper=True) def pytest_fixture_setup( - fixturedef: FixtureDef, request: SubRequest + fixturedef: FixtureDef[object], request: SubRequest ) -> Generator[None, None, None]: yield if request.config.option.setupshow: @@ -47,7 +47,7 @@ def pytest_fixture_setup( _show_fixture_action(fixturedef, "SETUP") -def pytest_fixture_post_finalizer(fixturedef: FixtureDef) -> None: +def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None: if fixturedef.cached_result is not None: config = fixturedef._fixturemanager.config if config.option.setupshow: @@ -56,7 +56,7 @@ def pytest_fixture_post_finalizer(fixturedef: FixtureDef) -> None: del fixturedef.cached_param # type: ignore[attr-defined] -def _show_fixture_action(fixturedef: FixtureDef, msg: str) -> None: +def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None: config = fixturedef._fixturemanager.config capman = config.pluginmanager.getplugin("capturemanager") if capman: diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 0994ebbf207..9ba81ccaf0a 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -22,7 +22,7 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(tryfirst=True) def pytest_fixture_setup( - fixturedef: FixtureDef, request: SubRequest + fixturedef: FixtureDef[object], request: SubRequest ) -> Optional[object]: # Will return a dummy fixture if the setuponly option is provided. if request.config.option.setupplan: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 86c327226d9..cb58f559546 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -319,7 +319,7 @@ def __init__(self, config: Config, file: Optional[TextIO] = None) -> None: self.stats = {} # type: Dict[str, List[Any]] self._main_color = None # type: Optional[str] - self._known_types = None # type: Optional[List] + self._known_types = None # type: Optional[List[str]] self.startdir = config.invocation_dir if file is None: file = sys.stdout @@ -469,7 +469,7 @@ def section(self, title: str, sep: str = "=", **kw: bool) -> None: def line(self, msg: str, **kw: bool) -> None: self._tw.line(msg, **kw) - def _add_stats(self, category: str, items: Sequence) -> None: + def _add_stats(self, category: str, items: Sequence[Any]) -> None: set_main_color = category not in self.stats self.stats.setdefault(category, []).extend(items) if set_main_color: diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 75c53a55277..09aa014c58a 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -141,7 +141,7 @@ def fixture(self, request: FixtureRequest) -> Generator[None, None, None]: class TestCaseFunction(Function): nofuncargs = True - _excinfo = None # type: Optional[List[_pytest._code.ExceptionInfo]] + _excinfo = None # type: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] _testcase = None # type: Optional[unittest.TestCase] def setup(self) -> None: @@ -279,7 +279,9 @@ def runtest(self) -> None: finally: delattr(self._testcase, self.name) - def _prunetraceback(self, excinfo: _pytest._code.ExceptionInfo) -> None: + def _prunetraceback( + self, excinfo: _pytest._code.ExceptionInfo[BaseException] + ) -> None: Function._prunetraceback(self, excinfo) traceback = excinfo.traceback.filter( lambda x: not x.frame.f_globals.get("__unittest") diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index d254dd3fb33..f7cf2533b99 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -144,7 +144,7 @@ class DummyFixtureDef: scope = attr.ib() fixtures_defs = cast( - Dict[str, Sequence[fixtures.FixtureDef]], + Dict[str, Sequence[fixtures.FixtureDef[object]]], dict( session_fix=[DummyFixtureDef("session")], package_fix=[DummyFixtureDef("package")], diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 6723a707e19..2adfb98a8d0 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -640,7 +640,8 @@ def test_frozenzet(self) -> None: def test_Sequence(self) -> None: # Test comparing with a Sequence subclass. - class TestSequence(collections.abc.MutableSequence): + # TODO(py36): Inherit from typing.MutableSequence[int]. + class TestSequence(collections.abc.MutableSequence): # type: ignore[type-arg] def __init__(self, iterable): self.elements = list(iterable) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 19aff99545c..a524fe0d86f 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1564,66 +1564,66 @@ def tr() -> TerminalReporter: # dict value, not the actual contents, so tuples of anything # suffice # Important statuses -- the highest priority of these always wins - ("red", [("1 failed", {"bold": True, "red": True})], {"failed": (1,)}), + ("red", [("1 failed", {"bold": True, "red": True})], {"failed": [1]}), ( "red", [ ("1 failed", {"bold": True, "red": True}), ("1 passed", {"bold": False, "green": True}), ], - {"failed": (1,), "passed": (1,)}, + {"failed": [1], "passed": [1]}, ), - ("red", [("1 error", {"bold": True, "red": True})], {"error": (1,)}), - ("red", [("2 errors", {"bold": True, "red": True})], {"error": (1, 2)}), + ("red", [("1 error", {"bold": True, "red": True})], {"error": [1]}), + ("red", [("2 errors", {"bold": True, "red": True})], {"error": [1, 2]}), ( "red", [ ("1 passed", {"bold": False, "green": True}), ("1 error", {"bold": True, "red": True}), ], - {"error": (1,), "passed": (1,)}, + {"error": [1], "passed": [1]}, ), # (a status that's not known to the code) - ("yellow", [("1 weird", {"bold": True, "yellow": True})], {"weird": (1,)}), + ("yellow", [("1 weird", {"bold": True, "yellow": True})], {"weird": [1]}), ( "yellow", [ ("1 passed", {"bold": False, "green": True}), ("1 weird", {"bold": True, "yellow": True}), ], - {"weird": (1,), "passed": (1,)}, + {"weird": [1], "passed": [1]}, ), - ("yellow", [("1 warning", {"bold": True, "yellow": True})], {"warnings": (1,)}), + ("yellow", [("1 warning", {"bold": True, "yellow": True})], {"warnings": [1]}), ( "yellow", [ ("1 passed", {"bold": False, "green": True}), ("1 warning", {"bold": True, "yellow": True}), ], - {"warnings": (1,), "passed": (1,)}, + {"warnings": [1], "passed": [1]}, ), ( "green", [("5 passed", {"bold": True, "green": True})], - {"passed": (1, 2, 3, 4, 5)}, + {"passed": [1, 2, 3, 4, 5]}, ), # "Boring" statuses. These have no effect on the color of the summary # line. Thus, if *every* test has a boring status, the summary line stays # at its default color, i.e. yellow, to warn the user that the test run # produced no useful information - ("yellow", [("1 skipped", {"bold": True, "yellow": True})], {"skipped": (1,)}), + ("yellow", [("1 skipped", {"bold": True, "yellow": True})], {"skipped": [1]}), ( "green", [ ("1 passed", {"bold": True, "green": True}), ("1 skipped", {"bold": False, "yellow": True}), ], - {"skipped": (1,), "passed": (1,)}, + {"skipped": [1], "passed": [1]}, ), ( "yellow", [("1 deselected", {"bold": True, "yellow": True})], - {"deselected": (1,)}, + {"deselected": [1]}, ), ( "green", @@ -1631,34 +1631,34 @@ def tr() -> TerminalReporter: ("1 passed", {"bold": True, "green": True}), ("1 deselected", {"bold": False, "yellow": True}), ], - {"deselected": (1,), "passed": (1,)}, + {"deselected": [1], "passed": [1]}, ), - ("yellow", [("1 xfailed", {"bold": True, "yellow": True})], {"xfailed": (1,)}), + ("yellow", [("1 xfailed", {"bold": True, "yellow": True})], {"xfailed": [1]}), ( "green", [ ("1 passed", {"bold": True, "green": True}), ("1 xfailed", {"bold": False, "yellow": True}), ], - {"xfailed": (1,), "passed": (1,)}, + {"xfailed": [1], "passed": [1]}, ), - ("yellow", [("1 xpassed", {"bold": True, "yellow": True})], {"xpassed": (1,)}), + ("yellow", [("1 xpassed", {"bold": True, "yellow": True})], {"xpassed": [1]}), ( "yellow", [ ("1 passed", {"bold": False, "green": True}), ("1 xpassed", {"bold": True, "yellow": True}), ], - {"xpassed": (1,), "passed": (1,)}, + {"xpassed": [1], "passed": [1]}, ), # Likewise if no tests were found at all ("yellow", [("no tests ran", {"yellow": True})], {}), # Test the empty-key special case - ("yellow", [("no tests ran", {"yellow": True})], {"": (1,)}), + ("yellow", [("no tests ran", {"yellow": True})], {"": [1]}), ( "green", [("1 passed", {"bold": True, "green": True})], - {"": (1,), "passed": (1,)}, + {"": [1], "passed": [1]}, ), # A couple more complex combinations ( @@ -1668,7 +1668,7 @@ def tr() -> TerminalReporter: ("2 passed", {"bold": False, "green": True}), ("3 xfailed", {"bold": False, "yellow": True}), ], - {"passed": (1, 2), "failed": (1,), "xfailed": (1, 2, 3)}, + {"passed": [1, 2], "failed": [1], "xfailed": [1, 2, 3]}, ), ( "green", @@ -1679,10 +1679,10 @@ def tr() -> TerminalReporter: ("2 xfailed", {"bold": False, "yellow": True}), ], { - "passed": (1,), - "skipped": (1, 2), - "deselected": (1, 2, 3), - "xfailed": (1, 2), + "passed": [1], + "skipped": [1, 2], + "deselected": [1, 2, 3], + "xfailed": [1, 2], }, ), ], @@ -1691,7 +1691,7 @@ def test_summary_stats( tr: TerminalReporter, exp_line: List[Tuple[str, Dict[str, bool]]], exp_color: str, - stats_arg: Dict[str, List], + stats_arg: Dict[str, List[object]], ) -> None: tr.stats = stats_arg From b8471aa527ab1a21ee66f24e5c23a9773b5b0793 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 18 Jul 2020 12:35:13 +0300 Subject: [PATCH 537/823] testing: fix some docstring issues In preparation for enforcing some docstring lints. --- doc/en/example/nonpython/conftest.py | 9 ++-- scripts/release.py | 4 +- testing/acceptance_test.py | 28 +++++-------- testing/code/test_excinfo.py | 2 +- testing/freeze/create_executable.py | 4 +- testing/logging/test_fixture.py | 8 ++-- testing/logging/test_reporting.py | 19 +++------ testing/python/approx.py | 4 +- testing/python/collect.py | 12 +++--- testing/python/fixtures.py | 10 ++--- testing/python/metafunc.py | 61 ++++++++++++---------------- testing/python/raises.py | 4 +- testing/test_argcomplete.py | 14 +++---- testing/test_assertion.py | 17 +++----- testing/test_assertrewrite.py | 11 ++--- testing/test_cacheprovider.py | 4 +- testing/test_capture.py | 9 ++-- testing/test_collection.py | 6 +-- testing/test_config.py | 14 +++---- testing/test_conftest.py | 4 +- testing/test_doctest.py | 20 +++------ testing/test_faulthandler.py | 14 +++---- testing/test_helpconfig.py | 6 +-- testing/test_junitxml.py | 17 ++++---- testing/test_link_resolve.py | 4 +- testing/test_mark.py | 18 ++++---- testing/test_meta.py | 3 +- testing/test_pastebin.py | 16 +++----- testing/test_pathlib.py | 11 +++-- testing/test_pluginmanager.py | 4 +- testing/test_pytester.py | 8 ++-- testing/test_reports.py | 7 +--- testing/test_runner.py | 15 +++---- testing/test_runner_xunit.py | 7 +--- testing/test_setuponly.py | 2 +- testing/test_setupplan.py | 9 ++-- testing/test_skipping.py | 21 +++------- testing/test_terminal.py | 21 ++++------ testing/test_unittest.py | 4 +- testing/test_warnings.py | 19 +++------ 40 files changed, 175 insertions(+), 295 deletions(-) diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py index d30ab3841dc..6e5a5709290 100644 --- a/doc/en/example/nonpython/conftest.py +++ b/doc/en/example/nonpython/conftest.py @@ -9,7 +9,8 @@ def pytest_collect_file(parent, path): class YamlFile(pytest.File): def collect(self): - import yaml # we need a yaml parser, e.g. PyYAML + # We need a yaml parser, e.g. PyYAML. + import yaml raw = yaml.safe_load(self.fspath.open()) for name, spec in sorted(raw.items()): @@ -23,12 +24,12 @@ def __init__(self, name, parent, spec): def runtest(self): for name, value in sorted(self.spec.items()): - # some custom test execution (dumb example follows) + # Some custom test execution (dumb example follows). if name != value: raise YamlException(self, name, value) def repr_failure(self, excinfo): - """ called when self.runtest() raises an exception. """ + """Called when self.runtest() raises an exception.""" if isinstance(excinfo.value, YamlException): return "\n".join( [ @@ -43,4 +44,4 @@ def reportinfo(self): class YamlException(Exception): - """ custom exception for error reporting. """ + """Custom exception for error reporting.""" diff --git a/scripts/release.py b/scripts/release.py index 443b868f3d7..5e3158ab52b 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,6 +1,4 @@ -""" -Invoke development tasks. -""" +"""Invoke development tasks.""" import argparse import os from pathlib import Path diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 2a386e2c6e4..3172dad7cc3 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -403,15 +403,12 @@ def pytest_sessionfinish(exitstatus): result.stdout.fnmatch_lines(["pytest_sessionfinish_called"]) assert result.ret == ExitCode.USAGE_ERROR - @pytest.mark.usefixtures("recwarn") def test_namespace_import_doesnt_confuse_import_hook(self, testdir): - """ - Ref #383. Python 3.3's namespace package messed with our import hooks + """Ref #383. + + Python 3.3's namespace package messed with our import hooks. Importing a module that didn't exist, even if the ImportError was gracefully handled, would make our test crash. - - Use recwarn here to silence this warning in Python 2.7: - ImportWarning: Not importing directory '...\not_a_package': missing __init__.py """ testdir.mkdir("not_a_package") p = testdir.makepyfile( @@ -457,10 +454,8 @@ def test_foo(invalid_fixture): ) def test_plugins_given_as_strings(self, tmpdir, monkeypatch, _sys_snapshot): - """test that str values passed to main() as `plugins` arg - are interpreted as module names to be imported and registered. - #855. - """ + """Test that str values passed to main() as `plugins` arg are + interpreted as module names to be imported and registered (#855).""" with pytest.raises(ImportError) as excinfo: pytest.main([str(tmpdir)], plugins=["invalid.module"]) assert "invalid" in str(excinfo.value) @@ -664,8 +659,7 @@ def test_cmdline_python_package(self, testdir, monkeypatch): result.stderr.fnmatch_lines(["*not*found*test_missing*"]) def test_cmdline_python_namespace_package(self, testdir, monkeypatch): - """ - test --pyargs option with namespace packages (#1567) + """Test --pyargs option with namespace packages (#1567). Ref: https://packaging.python.org/guides/packaging-namespace-packages/ """ @@ -1011,9 +1005,7 @@ def test_pytest_plugins_as_module(testdir): def test_deferred_hook_checking(testdir): - """ - Check hooks as late as possible (#1821). - """ + """Check hooks as late as possible (#1821).""" testdir.syspathinsert() testdir.makepyfile( **{ @@ -1089,8 +1081,7 @@ def test2(): def test_fixture_order_respects_scope(testdir): - """Ensure that fixtures are created according to scope order, regression test for #2405 - """ + """Ensure that fixtures are created according to scope order (#2405).""" testdir.makepyfile( """ import pytest @@ -1115,7 +1106,8 @@ def test_value(): def test_frame_leak_on_failing_test(testdir): - """pytest would leak garbage referencing the frames of tests that failed that could never be reclaimed (#2798) + """Pytest would leak garbage referencing the frames of tests that failed + that could never be reclaimed (#2798). Unfortunately it was not possible to remove the actual circles because most of them are made of traceback objects which cannot be weakly referenced. Those objects at least diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 78b55e26e01..75446c570b2 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -464,7 +464,7 @@ def f(x): assert lines[1] == " pass" def test_repr_source_excinfo(self) -> None: - """ check if indentation is right """ + """Check if indentation is right.""" try: def f(): diff --git a/testing/freeze/create_executable.py b/testing/freeze/create_executable.py index b53eb09f53b..998df7b1ca7 100644 --- a/testing/freeze/create_executable.py +++ b/testing/freeze/create_executable.py @@ -1,6 +1,4 @@ -""" -Generates an executable with pytest runner embedded using PyInstaller. -""" +"""Generate an executable with pytest runner embedded using PyInstaller.""" if __name__ == "__main__": import pytest import subprocess diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index 6e5e9c2b42a..cbd28f798bf 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -268,10 +268,10 @@ def test_log_level_override(request, caplog): def test_log_report_captures_according_to_config_option_upon_failure(testdir): - """ Test that upon failure: - (1) `caplog` succeeded to capture the DEBUG message and assert on it => No `Exception` is raised - (2) The `DEBUG` message does NOT appear in the `Captured log call` report - (3) The stdout, `INFO`, and `WARNING` messages DO appear in the test reports due to `--log-level=INFO` + """Test that upon failure: + (1) `caplog` succeeded to capture the DEBUG message and assert on it => No `Exception` is raised. + (2) The `DEBUG` message does NOT appear in the `Captured log call` report. + (3) The stdout, `INFO`, and `WARNING` messages DO appear in the test reports due to `--log-level=INFO`. """ testdir.makepyfile( """ diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 32224325884..bab28aea4ef 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -1066,10 +1066,8 @@ def test_second(): def test_colored_captured_log(testdir): - """ - Test that the level names of captured log messages of a failing test are - colored. - """ + """Test that the level names of captured log messages of a failing test + are colored.""" testdir.makepyfile( """ import logging @@ -1092,9 +1090,7 @@ def test_foo(): def test_colored_ansi_esc_caplogtext(testdir): - """ - Make sure that caplog.text does not contain ANSI escape sequences. - """ + """Make sure that caplog.text does not contain ANSI escape sequences.""" testdir.makepyfile( """ import logging @@ -1111,8 +1107,7 @@ def test_foo(caplog): def test_logging_emit_error(testdir: Testdir) -> None: - """ - An exception raised during emit() should fail the test. + """An exception raised during emit() should fail the test. The default behavior of logging is to print "Logging error" to stderr with the call stack and some extra details. @@ -1138,10 +1133,8 @@ def test_bad_log(): def test_logging_emit_error_supressed(testdir: Testdir) -> None: - """ - If logging is configured to silently ignore errors, pytest - doesn't propagate errors either. - """ + """If logging is configured to silently ignore errors, pytest + doesn't propagate errors either.""" testdir.makepyfile( """ import logging diff --git a/testing/python/approx.py b/testing/python/approx.py index db67fe5aa7f..194423dc3b0 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -488,9 +488,7 @@ def test_expected_value_type_error(self, x): ], ) def test_comparison_operator_type_error(self, op): - """ - pytest.approx should raise TypeError for operators other than == and != (#2003). - """ + """pytest.approx should raise TypeError for operators other than == and != (#2003).""" with pytest.raises(TypeError): op(1, approx(1, rel=1e-6, abs=1e-12)) diff --git a/testing/python/collect.py b/testing/python/collect.py index ed778c265ff..778ceeddf8d 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -984,8 +984,7 @@ def test_traceback_error_during_import(self, testdir): result.stdout.fnmatch_lines([">*asd*", "E*NameError*"]) def test_traceback_filter_error_during_fixture_collection(self, testdir): - """integration test for issue #995. - """ + """Integration test for issue #995.""" testdir.makepyfile( """ import pytest @@ -1011,8 +1010,9 @@ def test_failing_fixture(fail_fixture): result.stdout.fnmatch_lines(["*ValueError: fail me*", "* 1 error in *"]) def test_filter_traceback_generated_code(self) -> None: - """test that filter_traceback() works with the fact that + """Test that filter_traceback() works with the fact that _pytest._code.code.Code.path attribute might return an str object. + In this case, one of the entries on the traceback was produced by dynamically generated code. See: https://bitbucket.org/pytest-dev/py/issues/71 @@ -1033,8 +1033,9 @@ def test_filter_traceback_generated_code(self) -> None: assert not filter_traceback(traceback[-1]) def test_filter_traceback_path_no_longer_valid(self, testdir) -> None: - """test that filter_traceback() works with the fact that + """Test that filter_traceback() works with the fact that _pytest._code.code.Code.path attribute might return an str object. + In this case, one of the files in the traceback no longer exists. This fixes #1133. """ @@ -1250,8 +1251,7 @@ def test_injection(self): def test_syntax_error_with_non_ascii_chars(testdir): - """Fix decoding issue while formatting SyntaxErrors during collection (#578) - """ + """Fix decoding issue while formatting SyntaxErrors during collection (#578).""" testdir.makepyfile("☃") result = testdir.runpytest() result.stdout.fnmatch_lines(["*ERROR collecting*", "*SyntaxError*", "*1 error in*"]) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 70370915b0e..8ea1a75ff6f 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1684,10 +1684,8 @@ def test_func2(request): reprec.assertoutcome(passed=2) def test_callables_nocode(self, testdir): - """ - an imported mock.call would break setup/factory discovery - due to it being callable and __code__ not being a code object - """ + """An imported mock.call would break setup/factory discovery due to + it being callable and __code__ not being a code object.""" testdir.makepyfile( """ class _call(tuple): @@ -3333,9 +3331,7 @@ def fixture1(self): ) def test_show_fixtures_different_files(self, testdir): - """ - #833: --fixtures only shows fixtures from first file - """ + """`--fixtures` only shows fixtures from first file (#833).""" testdir.makepyfile( test_a=''' import pytest diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index f7cf2533b99..7aa608a0423 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -29,9 +29,9 @@ class TestMetafunc: def Metafunc(self, func, config=None) -> python.Metafunc: - # the unit tests of this class check if things work correctly + # The unit tests of this class check if things work correctly # on the funcarg level, so we don't need a full blown - # initialization + # initialization. class FuncFixtureInfoMock: name2fixturedefs = None @@ -136,7 +136,7 @@ def func(request): metafunc.parametrize("request", [1]) def test_find_parametrized_scope(self) -> None: - """unittest for _find_parametrized_scope (#3941)""" + """Unit test for _find_parametrized_scope (#3941).""" from _pytest.python import _find_parametrized_scope @attr.s @@ -285,30 +285,29 @@ def test_idval_hypothesis(self, value) -> None: escaped.encode("ascii") def test_unicode_idval(self) -> None: - """This tests that Unicode strings outside the ASCII character set get + """Test that Unicode strings outside the ASCII character set get escaped, using byte escapes if they're in that range or unicode escapes if they're not. """ values = [ - ("", ""), - ("ascii", "ascii"), - ("ação", "a\\xe7\\xe3o"), - ("josé@blah.com", "jos\\xe9@blah.com"), + ("", r""), + ("ascii", r"ascii"), + ("ação", r"a\xe7\xe3o"), + ("josé@blah.com", r"jos\xe9@blah.com"), ( - "δοκ.ιμή@παράδειγμα.δοκιμή", - "\\u03b4\\u03bf\\u03ba.\\u03b9\\u03bc\\u03ae@\\u03c0\\u03b1\\u03c1\\u03ac\\u03b4\\u03b5\\u03b9\\u03b3" - "\\u03bc\\u03b1.\\u03b4\\u03bf\\u03ba\\u03b9\\u03bc\\u03ae", + r"δοκ.ιμή@παράδειγμα.δοκιμή", + r"\u03b4\u03bf\u03ba.\u03b9\u03bc\u03ae@\u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3" + r"\u03bc\u03b1.\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae", ), ] for val, expected in values: assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected def test_unicode_idval_with_config(self) -> None: - """unittest for expected behavior to obtain ids with + """Unit test for expected behavior to obtain ids with disable_test_id_escaping_and_forfeit_all_rights_to_community_support - option. (#5294) - """ + option (#5294).""" class MockConfig: def __init__(self, config): @@ -335,25 +334,20 @@ def getini(self, name): assert actual == expected def test_bytes_idval(self) -> None: - """unittest for the expected behavior to obtain ids for parametrized - bytes values: - - python2: non-ascii strings are considered bytes and formatted using - "binary escape", where any byte < 127 is escaped into its hex form. - - python3: bytes objects are always escaped using "binary escape". - """ + """Unit test for the expected behavior to obtain ids for parametrized + bytes values: bytes objects are always escaped using "binary escape".""" values = [ - (b"", ""), - (b"\xc3\xb4\xff\xe4", "\\xc3\\xb4\\xff\\xe4"), - (b"ascii", "ascii"), - ("αρά".encode(), "\\xce\\xb1\\xcf\\x81\\xce\\xac"), + (b"", r""), + (b"\xc3\xb4\xff\xe4", r"\xc3\xb4\xff\xe4"), + (b"ascii", r"ascii"), + ("αρά".encode(), r"\xce\xb1\xcf\x81\xce\xac"), ] for val, expected in values: assert _idval(val, "a", 6, idfn=None, nodeid=None, config=None) == expected def test_class_or_function_idval(self) -> None: - """unittest for the expected behavior to obtain ids for parametrized - values that are classes or functions: their __name__. - """ + """Unit test for the expected behavior to obtain ids for parametrized + values that are classes or functions: their __name__.""" class TestClass: pass @@ -484,9 +478,9 @@ def ids(val: object) -> str: assert result == ["a-a0", "a-a1", "a-a2"] def test_idmaker_with_idfn_and_config(self) -> None: - """unittest for expected behavior to create ids with idfn and + """Unit test for expected behavior to create ids with idfn and disable_test_id_escaping_and_forfeit_all_rights_to_community_support - option. (#5294) + option (#5294). """ class MockConfig: @@ -516,9 +510,9 @@ def getini(self, name): assert result == [expected] def test_idmaker_with_ids_and_config(self) -> None: - """unittest for expected behavior to create ids with ids and + """Unit test for expected behavior to create ids with ids and disable_test_id_escaping_and_forfeit_all_rights_to_community_support - option. (#5294) + option (#5294). """ class MockConfig: @@ -1420,9 +1414,8 @@ def test_foo(x): class TestMetafuncFunctionalAuto: - """ - Tests related to automatically find out the correct scope for parametrized tests (#1832). - """ + """Tests related to automatically find out the correct scope for + parametrized tests (#1832).""" def test_parametrize_auto_scope(self, testdir: Testdir) -> None: testdir.makepyfile( diff --git a/testing/python/raises.py b/testing/python/raises.py index 46b200921e3..26931a37844 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -157,9 +157,7 @@ def test_no_raise_message(self) -> None: @pytest.mark.parametrize("method", ["function", "function_match", "with"]) def test_raises_cyclic_reference(self, method): - """ - Ensure pytest.raises does not leave a reference cycle (#1965). - """ + """Ensure pytest.raises does not leave a reference cycle (#1965).""" import gc class T: diff --git a/testing/test_argcomplete.py b/testing/test_argcomplete.py index 08362c62a63..9cab242c0e0 100644 --- a/testing/test_argcomplete.py +++ b/testing/test_argcomplete.py @@ -3,7 +3,7 @@ import pytest -# test for _argcomplete but not specific for any application +# Test for _argcomplete but not specific for any application. def equal_with_bash(prefix, ffc, fc, out=None): @@ -18,9 +18,9 @@ def equal_with_bash(prefix, ffc, fc, out=None): return retval -# copied from argcomplete.completers as import from there -# also pulls in argcomplete.__init__ which opens filedescriptor 9 -# this gives an OSError at the end of testrun +# Copied from argcomplete.completers as import from there. +# Also pulls in argcomplete.__init__ which opens filedescriptor 9. +# This gives an OSError at the end of testrun. def _wrapcall(*args, **kargs): @@ -31,7 +31,7 @@ def _wrapcall(*args, **kargs): class FilesCompleter: - "File completer class, optionally takes a list of allowed extensions" + """File completer class, optionally takes a list of allowed extensions.""" def __init__(self, allowednames=(), directories=True): # Fix if someone passes in a string instead of a list @@ -91,9 +91,7 @@ def test_compare_with_compgen(self, tmpdir): @pytest.mark.skipif("sys.platform in ('win32', 'darwin')") def test_remove_dir_prefix(self): - """this is not compatible with compgen but it is with bash itself: - ls /usr/ - """ + """This is not compatible with compgen but it is with bash itself: ls /usr/.""" from _pytest._argcomplete import FastFilesCompleter ffc = FastFilesCompleter() diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 2adfb98a8d0..1cb63a329f6 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -29,9 +29,7 @@ class TestImportHookInstallation: @pytest.mark.parametrize("initial_conftest", [True, False]) @pytest.mark.parametrize("mode", ["plain", "rewrite"]) def test_conftest_assertion_rewrite(self, testdir, initial_conftest, mode): - """Test that conftest files are using assertion rewrite on import. - (#1619) - """ + """Test that conftest files are using assertion rewrite on import (#1619).""" testdir.tmpdir.join("foo/tests").ensure(dir=1) conftest_path = "conftest.py" if initial_conftest else "foo/conftest.py" contents = { @@ -569,7 +567,7 @@ def test_dict_omitting(self) -> None: assert "b" not in line def test_dict_omitting_with_verbosity_1(self) -> None: - """ Ensure differing items are visible for verbosity=1 (#1512) """ + """Ensure differing items are visible for verbosity=1 (#1512).""" lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=1) assert lines is not None assert lines[1].startswith("Omitting 1 identical item") @@ -719,10 +717,7 @@ def __repr__(self): ] def test_one_repr_empty(self): - """ - the faulty empty string repr did trigger - an unbound local error in _diff_text - """ + """The faulty empty string repr did trigger an unbound local error in _diff_text.""" class A(str): def __repr__(self): @@ -1135,7 +1130,7 @@ def test_truncates_at_1_line_when_first_line_is_GT_max_chars(self): assert last_line_before_trunc_msg.endswith("...") def test_full_output_truncated(self, monkeypatch, testdir): - """ Test against full runpytest() output. """ + """Test against full runpytest() output.""" line_count = 7 line_len = 100 @@ -1370,9 +1365,7 @@ def test_onefails(): def test_exception_handling_no_traceback(testdir): - """ - Handle chain exceptions in tasks submitted by the multiprocess module (#1984). - """ + """Handle chain exceptions in tasks submitted by the multiprocess module (#1984).""" p1 = testdir.makepyfile( """ from multiprocessing import Pool diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index e403bb2ec9b..23f535173c2 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -233,7 +233,7 @@ def f() -> None: def test_dont_rewrite_if_hasattr_fails(self, request) -> None: class Y: - """ A class whos getattr fails, but not with `AttributeError` """ + """A class whose getattr fails, but not with `AttributeError`.""" def __getattr__(self, attribute_name): raise KeyError() @@ -911,10 +911,8 @@ def test_rewritten(): assert testdir.runpytest_subprocess().ret == 0 def test_remember_rewritten_modules(self, pytestconfig, testdir, monkeypatch): - """ - AssertionRewriteHook should remember rewritten modules so it - doesn't give false positives (#2005). - """ + """`AssertionRewriteHook` should remember rewritten modules so it + doesn't give false positives (#2005).""" monkeypatch.syspath_prepend(testdir.tmpdir) testdir.makepyfile(test_remember_rewritten_modules="") warnings = [] @@ -1091,8 +1089,7 @@ def test_loader(): result.stdout.fnmatch_lines(["* 1 passed*"]) def test_get_data_support(self, testdir): - """Implement optional PEP302 api (#808). - """ + """Implement optional PEP302 api (#808).""" path = testdir.mkpydir("foo") path.join("test_foo.py").write( textwrap.dedent( diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index c133663ea1b..a911257ce24 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -643,9 +643,7 @@ def get_cached_last_failed(self, testdir): return sorted(config.cache.get("cache/lastfailed", {})) def test_cache_cumulative(self, testdir): - """ - Test workflow where user fixes errors gradually file by file using --lf. - """ + """Test workflow where user fixes errors gradually file by file using --lf.""" # 1. initial run test_bar = testdir.makepyfile( test_bar=""" diff --git a/testing/test_capture.py b/testing/test_capture.py index bc89501c73b..15077a3e974 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -635,9 +635,8 @@ def test_normal(): @pytest.mark.parametrize("fixture", ["capsys", "capfd"]) def test_fixture_use_by_other_fixtures(self, testdir, fixture): - """ - Ensure that capsys and capfd can be used by other fixtures during setup and teardown. - """ + """Ensure that capsys and capfd can be used by other fixtures during + setup and teardown.""" testdir.makepyfile( """\ import sys @@ -1109,8 +1108,8 @@ class TestTeeStdCapture(TestStdCapture): captureclass = staticmethod(TeeStdCapture) def test_capturing_error_recursive(self): - """ for TeeStdCapture since we passthrough stderr/stdout, cap1 - should get all output, while cap2 should only get "cap2\n" """ + r"""For TeeStdCapture since we passthrough stderr/stdout, cap1 + should get all output, while cap2 should only get "cap2\n".""" with self.getcapture() as cap1: print("cap1") diff --git a/testing/test_collection.py b/testing/test_collection.py index f5e8abfd727..9f22f3ee038 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -725,10 +725,8 @@ def testmethod_two(self, arg0): print(s) def test_class_and_functions_discovery_using_glob(self, testdir): - """ - tests that python_classes and python_functions config options work - as prefixes and glob-like patterns (issue #600). - """ + """Test that Python_classes and Python_functions config options work + as prefixes and glob-like patterns (#600).""" testdir.makeini( """ [pytest] diff --git a/testing/test_config.py b/testing/test_config.py index 9b1c11d5edc..26d2a3ef09b 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -47,7 +47,7 @@ def test_getcfg_and_config(self, testdir, tmpdir, section, filename): assert config.inicfg["name"] == "value" def test_getcfg_empty_path(self): - """correctly handle zero length arguments (a la pytest '')""" + """Correctly handle zero length arguments (a la pytest '').""" locate_config([""]) def test_setupcfg_uses_toolpytest_with_pytest(self, testdir): @@ -1006,8 +1006,8 @@ def pytest_cmdline_preparse(args): def test_invalid_options_show_extra_information(testdir): - """display extra information when pytest exits due to unrecognized - options in the command-line""" + """Display extra information when pytest exits due to unrecognized + options in the command-line.""" testdir.makeini( """ [pytest] @@ -1441,7 +1441,7 @@ def test_addopts_from_env_not_concatenated(self, monkeypatch, _config_for_test): ) def test_addopts_from_ini_not_concatenated(self, testdir): - """addopts from ini should not take values from normal args (#4265).""" + """`addopts` from ini should not take values from normal args (#4265).""" testdir.makeini( """ [pytest] @@ -1777,10 +1777,8 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives def test_conftest_import_error_repr(tmpdir): - """ - ConftestImportFailure should use a short error message and readable path to the failed - conftest.py file - """ + """`ConftestImportFailure` should use a short error message and readable + path to the failed conftest.py file.""" path = tmpdir.join("foo/conftest.py") with pytest.raises( ConftestImportFailure, diff --git a/testing/test_conftest.py b/testing/test_conftest.py index dbafe7dd34b..d1a69f4babc 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -196,9 +196,7 @@ def pytest_addoption(parser): def test_conftest_symlink(testdir): - """ - conftest.py discovery follows normal path resolution and does not resolve symlinks. - """ + """`conftest.py` discovery follows normal path resolution and does not resolve symlinks.""" # Structure: # /real # /real/conftest.py diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 965dba6c179..0b32ad32203 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -115,8 +115,7 @@ def test_new_pattern(self, testdir): reprec.assertoutcome(failed=1) def test_multiple_patterns(self, testdir): - """Test support for multiple --doctest-glob arguments (#1255). - """ + """Test support for multiple --doctest-glob arguments (#1255).""" testdir.maketxtfile( xdoc=""" >>> 1 @@ -149,8 +148,7 @@ def test_multiple_patterns(self, testdir): [("foo", "ascii"), ("öäü", "latin1"), ("öäü", "utf-8")], ) def test_encoding(self, testdir, test_string, encoding): - """Test support for doctest_encoding ini option. - """ + """Test support for doctest_encoding ini option.""" testdir.makeini( """ [pytest] @@ -667,8 +665,7 @@ def test_non_ignored_whitespace_glob(self, testdir): reprec.assertoutcome(failed=1, passed=0) def test_contains_unicode(self, testdir): - """Fix internal error with docstrings containing non-ascii characters. - """ + """Fix internal error with docstrings containing non-ascii characters.""" testdir.makepyfile( '''\ def foo(): @@ -701,9 +698,7 @@ def add_one(x): reprec.assertoutcome(skipped=1, failed=1, passed=0) def test_junit_report_for_doctest(self, testdir): - """ - #713: Fix --junit-xml option when used with --doctest-modules. - """ + """#713: Fix --junit-xml option when used with --doctest-modules.""" p = testdir.makepyfile( """ def foo(): @@ -775,9 +770,7 @@ def test_print_unicode_value(self, testdir): result.stdout.fnmatch_lines(["* 1 passed *"]) def test_reportinfo(self, testdir): - """ - Test case to make sure that DoctestItem.reportinfo() returns lineno. - """ + """Make sure that DoctestItem.reportinfo() returns lineno.""" p = testdir.makepyfile( test_reportinfo=""" def foo(x): @@ -1167,8 +1160,7 @@ class TestDoctestAutoUseFixtures: SCOPES = ["module", "session", "class", "function"] def test_doctest_module_session_fixture(self, testdir): - """Test that session fixtures are initialized for doctest modules (#768) - """ + """Test that session fixtures are initialized for doctest modules (#768).""" # session fixture which changes some global data, which will # be accessed by doctests in a module testdir.makeconftest( diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index 46adccd2159..87a195bf807 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -19,8 +19,7 @@ def test_crash(): def test_crash_near_exit(testdir): """Test that fault handler displays crashes that happen even after - pytest is exiting (for example, when the interpreter is shutting down). - """ + pytest is exiting (for example, when the interpreter is shutting down).""" testdir.makepyfile( """ import faulthandler @@ -35,8 +34,7 @@ def test_ok(): def test_disabled(testdir): - """Test option to disable fault handler in the command line. - """ + """Test option to disable fault handler in the command line.""" testdir.makepyfile( """ import faulthandler @@ -60,6 +58,7 @@ def test_disabled(): ) def test_timeout(testdir, enabled: bool) -> None: """Test option to dump tracebacks after a certain timeout. + If faulthandler is disabled, no traceback will be dumped. """ testdir.makepyfile( @@ -90,9 +89,8 @@ def test_timeout(): @pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"]) def test_cancel_timeout_on_hook(monkeypatch, hook_name): """Make sure that we are cancelling any scheduled traceback dumping due - to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any other interactive - exception (pytest-dev/pytest-faulthandler#14). - """ + to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any + other interactive exception (pytest-dev/pytest-faulthandler#14).""" import faulthandler from _pytest.faulthandler import FaultHandlerHooks @@ -111,7 +109,7 @@ def test_cancel_timeout_on_hook(monkeypatch, hook_name): @pytest.mark.parametrize("faulthandler_timeout", [0, 2]) def test_already_initialized(faulthandler_timeout, testdir): - """Test for faulthandler being initialized earlier than pytest (#6575)""" + """Test for faulthandler being initialized earlier than pytest (#6575).""" testdir.makepyfile( """ def test(): diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index a33273a2c1d..6116242ec0d 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -39,8 +39,7 @@ def test_help(testdir): def test_none_help_param_raises_exception(testdir): - """Tests a None help param raises a TypeError. - """ + """Test that a None help param raises a TypeError.""" testdir.makeconftest( """ def pytest_addoption(parser): @@ -54,8 +53,7 @@ def pytest_addoption(parser): def test_empty_help_param(testdir): - """Tests an empty help param is displayed correctly. - """ + """Test that an empty help param is displayed correctly.""" testdir.makeconftest( """ def pytest_addoption(parser): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 5487940fbcd..3cc93a39805 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -22,7 +22,7 @@ @pytest.fixture(scope="session") def schema(): - """Returns a xmlschema.XMLSchema object for the junit-10.xsd file""" + """Return an xmlschema.XMLSchema object for the junit-10.xsd file.""" fn = Path(__file__).parent / "example_scripts/junit-10.xsd" with fn.open() as f: return xmlschema.XMLSchema(f) @@ -30,9 +30,8 @@ def schema(): @pytest.fixture def run_and_parse(testdir, schema): - """ - Fixture that returns a function that can be used to execute pytest and return - the parsed ``DomNode`` of the root xml node. + """Fixture that returns a function that can be used to execute pytest and + return the parsed ``DomNode`` of the root xml node. The ``family`` parameter is used to configure the ``junit_family`` of the written report. "xunit2" is also automatically validated against the schema. @@ -720,7 +719,7 @@ def test_hello(): assert "hx" in fnode.toxml() def test_assertion_binchars(self, testdir, run_and_parse): - """this test did fail when the escaping wasnt strict""" + """This test did fail when the escaping wasn't strict.""" testdir.makepyfile( """ @@ -1212,8 +1211,7 @@ def test_record(record_xml_attribute, other): @pytest.mark.filterwarnings("default") @pytest.mark.parametrize("fixture_name", ["record_xml_attribute", "record_property"]) def test_record_fixtures_xunit2(testdir, fixture_name, run_and_parse): - """Ensure record_xml_attribute and record_property drop values when outside of legacy family - """ + """Ensure record_xml_attribute and record_property drop values when outside of legacy family.""" testdir.makeini( """ [pytest] @@ -1250,10 +1248,9 @@ def test_record({fixture_name}, other): def test_random_report_log_xdist(testdir, monkeypatch, run_and_parse): - """xdist calls pytest_runtest_logreport as they are executed by the workers, + """`xdist` calls pytest_runtest_logreport as they are executed by the workers, with nodes from several nodes overlapping, so junitxml must cope with that - to produce correct reports. #1064 - """ + to produce correct reports (#1064).""" pytest.importorskip("xdist") monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) testdir.makepyfile( diff --git a/testing/test_link_resolve.py b/testing/test_link_resolve.py index 3e9199dff56..f43f7ded567 100644 --- a/testing/test_link_resolve.py +++ b/testing/test_link_resolve.py @@ -50,9 +50,7 @@ def subst_path_linux(filename): def test_link_resolve(testdir: pytester.Testdir) -> None: - """ - See: https://github.com/pytest-dev/pytest/issues/5965 - """ + """See: https://github.com/pytest-dev/pytest/issues/5965.""" sub1 = testdir.mkpydir("sub1") p = sub1.join("test_foo.py") p.write( diff --git a/testing/test_mark.py b/testing/test_mark.py index f00c1330e65..5d5e0cf42f7 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -370,9 +370,8 @@ def test_func(arg): def test_parametrized_collected_from_command_line(testdir): - """Parametrized test not collected if test named specified - in command line issue#649. - """ + """Parametrized test not collected if test named specified in command + line issue#649.""" py_file = testdir.makepyfile( """ import pytest @@ -430,7 +429,7 @@ def test_func(a, b): def test_parametrize_iterator(testdir): - """parametrize should work with generators (#5354).""" + """`parametrize` should work with generators (#5354).""" py_file = testdir.makepyfile( """\ import pytest @@ -669,13 +668,12 @@ def test_some(request): reprec.assertoutcome(passed=1) def assert_markers(self, items, **expected): - """assert that given items have expected marker names applied to them. - expected should be a dict of (item name -> seq of expected marker names) + """Assert that given items have expected marker names applied to them. + expected should be a dict of (item name -> seq of expected marker names). - .. note:: this could be moved to ``testdir`` if proven to be useful + Note: this could be moved to ``testdir`` if proven to be useful to other modules. """ - items = {x.name: x for x in items} for name, expected_markers in expected.items(): markers = {m.name for m in items[name].iter_markers()} @@ -866,9 +864,7 @@ def test_one(): assert 1 assert len(deselected_tests) == 1 def test_no_match_directories_outside_the_suite(self, testdir): - """ - -k should not match against directories containing the test suite (#7040). - """ + """`-k` should not match against directories containing the test suite (#7040).""" test_contents = """ def test_aaa(): pass def test_ddd(): pass diff --git a/testing/test_meta.py b/testing/test_meta.py index 7ab8951a015..1acf6d09f59 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -1,5 +1,4 @@ -""" -Test importing of all internal packages and modules. +"""Test importing of all internal packages and modules. This ensures all internal packages can be imported without needing the pytest namespace being set, which is critical for the initialization of xdist. diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 0701641f805..7f88a13eb43 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -87,9 +87,7 @@ def pastebin(self, request): @pytest.fixture def mocked_urlopen_fail(self, monkeypatch): - """ - monkeypatch the actual urlopen call to emulate a HTTP Error 400 - """ + """Monkeypatch the actual urlopen call to emulate a HTTP Error 400.""" calls = [] import urllib.error @@ -104,11 +102,9 @@ def mocked(url, data): @pytest.fixture def mocked_urlopen_invalid(self, monkeypatch): - """ - monkeypatch the actual urlopen calls done by the internal plugin + """Monkeypatch the actual urlopen calls done by the internal plugin function that connects to bpaste service, but return a url in an - unexpected format - """ + unexpected format.""" calls = [] def mocked(url, data): @@ -128,10 +124,8 @@ def read(self): @pytest.fixture def mocked_urlopen(self, monkeypatch): - """ - monkeypatch the actual urlopen calls done by the internal plugin - function that connects to bpaste service. - """ + """Monkeypatch the actual urlopen calls done by the internal plugin + function that connects to bpaste service.""" calls = [] def mocked(url, data): diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 2c1a1c021f8..74dac21d996 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -18,9 +18,8 @@ class TestFNMatcherPort: - """Test that our port of py.common.FNMatcher (fnmatch_ex) produces the same results as the - original py.path.local.fnmatch method. - """ + """Test that our port of py.common.FNMatcher (fnmatch_ex) produces the + same results as the original py.path.local.fnmatch method.""" @pytest.fixture(params=["pathlib", "py.path"]) def match(self, request): @@ -268,19 +267,19 @@ def foo(x): return 40 + x return fn def test_importmode_importlib(self, simple_module): - """importlib mode does not change sys.path""" + """`importlib` mode does not change sys.path.""" module = import_path(simple_module, mode="importlib") assert module.foo(2) == 42 # type: ignore[attr-defined] assert simple_module.dirname not in sys.path def test_importmode_twice_is_different_module(self, simple_module): - """importlib mode always returns a new module""" + """`importlib` mode always returns a new module.""" module1 = import_path(simple_module, mode="importlib") module2 = import_path(simple_module, mode="importlib") assert module1 is not module2 def test_no_meta_path_found(self, simple_module, monkeypatch): - """Even without any meta_path should still import module""" + """Even without any meta_path should still import module.""" monkeypatch.setattr(sys, "meta_path", []) module = import_path(simple_module, mode="importlib") assert module.foo(2) == 42 # type: ignore[attr-defined] diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 448900501b1..a083f4b4f37 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -362,10 +362,10 @@ def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm): def test_plugin_prevent_register_stepwise_on_cacheprovider_unregister( self, pytestpm ): - """ From PR #4304 : The only way to unregister a module is documented at + """From PR #4304: The only way to unregister a module is documented at the end of https://docs.pytest.org/en/stable/plugins.html. - When unregister cacheprovider, then unregister stepwise too + When unregister cacheprovider, then unregister stepwise too. """ pytestpm.register(42, name="cacheprovider") pytestpm.register(43, name="stepwise") diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 46f3e1cabfa..5259b448418 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -166,18 +166,18 @@ def test_potato(): def make_holder(): class apiclass: def pytest_xyz(self, arg): - "x" + """X""" def pytest_xyz_noarg(self): - "x" + """X""" apimod = type(os)("api") def pytest_xyz(arg): - "x" + """X""" def pytest_xyz_noarg(): - "x" + """X""" apimod.pytest_xyz = pytest_xyz # type: ignore apimod.pytest_xyz_noarg = pytest_xyz_noarg # type: ignore diff --git a/testing/test_reports.py b/testing/test_reports.py index 08ac014a40b..a47d5378705 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -9,8 +9,7 @@ class TestReportSerialization: def test_xdist_longrepr_to_str_issue_241(self, testdir): - """ - Regarding issue pytest-xdist#241 + """Regarding issue pytest-xdist#241. This test came originally from test_remote.py in xdist (ca03269). """ @@ -133,9 +132,7 @@ def test_repr_entry_native(): assert rep_entries[i].lines == a_entries[i].lines def test_itemreport_outcomes(self, testdir): - """ - This test came originally from test_remote.py in xdist (ca03269). - """ + # This test came originally from test_remote.py in xdist (ca03269). reprec = testdir.inline_runsource( """ import pytest diff --git a/testing/test_runner.py b/testing/test_runner.py index b207ccc927f..b9d22370a7b 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -310,7 +310,7 @@ def teardown_function(func): assert reps[5].failed def test_exact_teardown_issue1206(self, testdir) -> None: - """issue shadowing error with wrong number of arguments on teardown_method.""" + """Issue shadowing error with wrong number of arguments on teardown_method.""" rec = testdir.inline_runsource( """ import pytest @@ -742,7 +742,7 @@ def test_importorskip_dev_module(monkeypatch) -> None: def test_importorskip_module_level(testdir) -> None: - """importorskip must be able to skip entire modules when used at module level""" + """`importorskip` must be able to skip entire modules when used at module level.""" testdir.makepyfile( """ import pytest @@ -757,7 +757,7 @@ def test_foo(): def test_importorskip_custom_reason(testdir) -> None: - """make sure custom reasons are used""" + """Make sure custom reasons are used.""" testdir.makepyfile( """ import pytest @@ -871,9 +871,8 @@ def test_fix(foo): def test_store_except_info_on_error() -> None: - """ Test that upon test failure, the exception info is stored on - sys.last_traceback and friends. - """ + """Test that upon test failure, the exception info is stored on + sys.last_traceback and friends.""" # Simulate item that might raise a specific exception, depending on `raise_error` class var class ItemMightRaise: nodeid = "item_that_raises" @@ -934,9 +933,7 @@ def test(fix): class TestReportContents: - """ - Test user-level API of ``TestReport`` objects. - """ + """Test user-level API of ``TestReport`` objects.""" def getrunner(self): return lambda item: runner.runtestprotocol(item, log=False) diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index 1b5d9737177..1abb35043b7 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -1,7 +1,4 @@ -""" - test correct setup/teardowns at - module, class, and instance level -""" +"""Test correct setup/teardowns at module, class, and instance level.""" from typing import List import pytest @@ -246,7 +243,7 @@ def test_function2(hello): def test_setup_teardown_function_level_with_optional_argument( testdir, monkeypatch, arg: str, ) -> None: - """parameter to setup/teardown xunit-style functions parameter is now optional (#1728).""" + """Parameter to setup/teardown xunit-style functions parameter is now optional (#1728).""" import sys trace_setups_teardowns = [] # type: List[str] diff --git a/testing/test_setuponly.py b/testing/test_setuponly.py index 221c32a310b..a43c850696e 100644 --- a/testing/test_setuponly.py +++ b/testing/test_setuponly.py @@ -254,7 +254,7 @@ def test_capturing(two): def test_show_fixtures_and_execute_test(testdir): - """ Verifies that setups are shown and tests are executed. """ + """Verify that setups are shown and tests are executed.""" p = testdir.makepyfile( """ import pytest diff --git a/testing/test_setupplan.py b/testing/test_setupplan.py index 64b464b32dd..929e883cce2 100644 --- a/testing/test_setupplan.py +++ b/testing/test_setupplan.py @@ -1,5 +1,5 @@ def test_show_fixtures_and_test(testdir, dummy_yaml_custom_test): - """ Verifies that fixtures are not executed. """ + """Verify that fixtures are not executed.""" testdir.makepyfile( """ import pytest @@ -20,8 +20,7 @@ def test_arg(arg): def test_show_multi_test_fixture_setup_and_teardown_correctly_simple(testdir): - """ - Verify that when a fixture lives for longer than a single test, --setup-plan + """Verify that when a fixture lives for longer than a single test, --setup-plan correctly displays the SETUP/TEARDOWN indicators the right number of times. As reported in https://github.com/pytest-dev/pytest/issues/2049 @@ -68,9 +67,7 @@ def test_two(self, fix): def test_show_multi_test_fixture_setup_and_teardown_same_as_setup_show(testdir): - """ - Verify that SETUP/TEARDOWN messages match what comes out of --setup-show. - """ + """Verify that SETUP/TEARDOWN messages match what comes out of --setup-show.""" testdir.makepyfile( """ import pytest diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 92182ff382f..b32d2267d21 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -188,9 +188,7 @@ def test_func(): assert callreport.wasxfail == "this is an xfail" def test_xfail_using_platform(self, testdir): - """ - Verify that platform can be used with xfail statements. - """ + """Verify that platform can be used with xfail statements.""" item = testdir.getitem( """ import pytest @@ -476,9 +474,8 @@ def test_raises(): result.stdout.fnmatch_lines([matchline]) def test_strict_sanity(self, testdir): - """sanity check for xfail(strict=True): a failing test should behave - exactly like a normal xfail. - """ + """Sanity check for xfail(strict=True): a failing test should behave + exactly like a normal xfail.""" p = testdir.makepyfile( """ import pytest @@ -1137,9 +1134,7 @@ def pytest_collect_file(path, parent): def test_module_level_skip_error(testdir): - """ - Verify that using pytest.skip at module level causes a collection error - """ + """Verify that using pytest.skip at module level causes a collection error.""" testdir.makepyfile( """ import pytest @@ -1156,9 +1151,7 @@ def test_func(): def test_module_level_skip_with_allow_module_level(testdir): - """ - Verify that using pytest.skip(allow_module_level=True) is allowed - """ + """Verify that using pytest.skip(allow_module_level=True) is allowed.""" testdir.makepyfile( """ import pytest @@ -1173,9 +1166,7 @@ def test_func(): def test_invalid_skip_keyword_parameter(testdir): - """ - Verify that using pytest.skip() with unknown parameter raises an error - """ + """Verify that using pytest.skip() with unknown parameter raises an error.""" testdir.makepyfile( """ import pytest diff --git a/testing/test_terminal.py b/testing/test_terminal.py index a524fe0d86f..36604ece78a 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1,6 +1,4 @@ -""" -terminal reporting of the full testing process. -""" +"""Terminal reporting of the full testing process.""" import collections import os import sys @@ -440,10 +438,8 @@ def test_collectonly_error(self, testdir): ) def test_collectonly_missing_path(self, testdir): - """this checks issue 115, - failure in parseargs will cause session - not to have the items attribute - """ + """Issue 115: failure in parseargs will cause session not to + have the items attribute.""" result = testdir.runpytest("--collect-only", "uhm_missing_path") assert result.ret == 4 result.stderr.fnmatch_lines(["*ERROR: file not found*"]) @@ -531,7 +527,7 @@ def teardown_function(function): ) def test_setup_teardown_output_and_test_failure(self, testdir): - """ Test for issue #442 """ + """Test for issue #442.""" testdir.makepyfile( """ def setup_function(function): @@ -1076,9 +1072,7 @@ def test_color_no(testdir): @pytest.mark.parametrize("verbose", [True, False]) def test_color_yes_collection_on_non_atty(testdir, verbose): - """skip collect progress report when working on non-terminals. - #1397 - """ + """#1397: Skip collect progress report when working on non-terminals.""" testdir.makepyfile( """ import pytest @@ -1208,9 +1202,8 @@ def test_traceconfig(testdir): class TestGenericReporting: - """ this test class can be subclassed with a different option - provider to run e.g. distributed tests. - """ + """Test class which can be subclassed with a different option provider to + run e.g. distributed tests.""" def test_collect_fail(self, testdir, option): testdir.makepyfile("import xyz\n") diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 6ddc6186be4..f9455e63b42 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1196,9 +1196,7 @@ def test_2(self): @pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"]) def test_pdb_teardown_skipped(testdir, monkeypatch, mark: str) -> None: - """ - With --pdb, setUp and tearDown should not be called for skipped tests. - """ + """With --pdb, setUp and tearDown should not be called for skipped tests.""" tracked = [] # type: List[str] monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index c3668180216..d26c71ca3b2 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -13,9 +13,7 @@ @pytest.fixture def pyfile_with_warnings(testdir: Testdir, request: FixtureRequest) -> str: - """ - Create a test file which calls a function in a module which generates warnings. - """ + """Create a test file which calls a function in a module which generates warnings.""" testdir.syspathinsert() test_name = request.function.__name__ module_name = test_name.lstrip("test_") + "_module" @@ -42,9 +40,7 @@ def foo(): @pytest.mark.filterwarnings("default") def test_normal_flow(testdir, pyfile_with_warnings): - """ - Check that the warnings section is displayed. - """ + """Check that the warnings section is displayed.""" result = testdir.runpytest(pyfile_with_warnings) result.stdout.fnmatch_lines( [ @@ -180,9 +176,8 @@ def test_my_warning(self): @pytest.mark.parametrize("default_config", ["ini", "cmdline"]) def test_filterwarnings_mark(testdir, default_config): - """ - Test ``filterwarnings`` mark works and takes precedence over command line and ini options. - """ + """Test ``filterwarnings`` mark works and takes precedence over command + line and ini options.""" if default_config == "ini": testdir.makeini( """ @@ -305,9 +300,7 @@ def pytest_warning_recorded(self, warning_message, when, nodeid, location): @pytest.mark.filterwarnings("always") def test_collection_warnings(testdir): - """ - Check that we also capture warnings issued during test collection (#3251). - """ + """Check that we also capture warnings issued during test collection (#3251).""" testdir.makepyfile( """ import warnings @@ -387,7 +380,7 @@ def test_bar(): @pytest.mark.parametrize("ignore_on_cmdline", [True, False]) def test_option_precedence_cmdline_over_ini(testdir, ignore_on_cmdline): - """filters defined in the command-line should take precedence over filters in ini files (#3946).""" + """Filters defined in the command-line should take precedence over filters in ini files (#3946).""" testdir.makeini( """ [pytest] From 9a18b57c7cca54352dac7497a608925691b8d0bd Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 18 Jul 2020 12:28:44 +0300 Subject: [PATCH 538/823] Enforce some pydocstyle lints with flake8-docstrings There are some ones we *would* like to enforce, like D401 First line should be in imperative mood but have too many false positives, so I left them out. --- .pre-commit-config.yaml | 4 +++- tox.ini | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 817cee60406..6068a2d324d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,9 @@ repos: hooks: - id: flake8 language_version: python3 - additional_dependencies: [flake8-typing-imports==1.9.0] + additional_dependencies: + - flake8-typing-imports==1.9.0 + - flake8-docstrings==1.5.0 - repo: https://github.com/asottile/reorder_python_imports rev: v2.3.0 hooks: diff --git a/tox.ini b/tox.ini index c8165a3c3bb..30aeb27be33 100644 --- a/tox.ini +++ b/tox.ini @@ -154,7 +154,17 @@ commands = python scripts/publish-gh-release-notes.py {posargs} [flake8] max-line-length = 120 -extend-ignore = E203 +extend-ignore = + ; whitespace before ':' + E203 + ; Missing Docstrings + D100,D101,D102,D103,D104,D105,D106,D107 + ; Whitespace Issues + D202,D203,D204,D205,D209,D213 + ; Quotes Issues + D302 + ; Docstring Content Issues + D400,D401,D401,D402,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D415,D416,D417 [isort] ; This config mimics what reorder-python-imports does. From 1c9b84756fe1d545f89de1337f8b6ae2d7543577 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Aug 2020 20:05:27 +0200 Subject: [PATCH 539/823] Properly remove log_print This is a follow up to 3f8200676f12846b74289f9b2e35747623fc768a which didn't make it clear that log_print is also removed in the changelog and didn't remove it from the reference docs. --- doc/en/changelog.rst | 4 ++-- doc/en/deprecations.rst | 5 +++-- doc/en/reference.rst | 14 -------------- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 7516b53f4ca..05ce4caea8a 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -212,9 +212,9 @@ Breaking Changes - `#7224 `_: The `item.catch_log_handler` and `item.catch_log_handlers` attributes, set by the - logging plugin and never meant to be public , are no longer available. + logging plugin and never meant to be public, are no longer available. - The deprecated ``--no-print-logs`` option is removed. Use ``--show-capture`` instead. + The deprecated ``--no-print-logs`` option and ``log_print`` ini option are removed. Use ``--show-capture`` instead. - `#7226 `_: Removed the unused ``args`` parameter from ``pytest.Function.__init__``. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index a2bed186256..3334b5d5fe4 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -51,9 +51,10 @@ a public API and may break in the future. .. versionremoved:: 6.0 -Option ``--no-print-logs`` is removed. If you used ``--no-print-logs``, please use ``--show-capture`` instead. +The ``--no-print-logs`` option and ``log_print`` ini setting are removed. If +you used them, please use ``--show-capture`` instead. -``--show-capture`` command-line option was added in ``pytest 3.5.0`` and allows to specify how to +A ``--show-capture`` command-line option was added in ``pytest 3.5.0`` which allows to specify how to display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default). diff --git a/doc/en/reference.rst b/doc/en/reference.rst index f4a68f16046..3bc7161aa7b 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1468,20 +1468,6 @@ passed multiple times. The expected format is ``name=value``. For example:: For more information, see :ref:`logging`. -.. confval:: log_print - - - - If set to ``False``, will disable displaying captured logging messages for failed tests. - - .. code-block:: ini - - [pytest] - log_print = False - - For more information, see :ref:`logging`. - - .. confval:: markers When the ``--strict-markers`` or ``--strict`` command-line arguments are used, From 9ab14c6d9cc8318f62d14e0c49ca37a13972bd0e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 3 Aug 2020 19:15:21 +0300 Subject: [PATCH 540/823] typing: set warn_unreachable This makes mypy raise an error whenever it detects code which is statically unreachable, e.g. x: int if isinstance(x, str): ... # Statement is unreachable [unreachable] This is really neat and finds quite a few logic and typing bugs. Sometimes the code is intentionally unreachable in terms of types, e.g. raising TypeError when a function is given an argument with a wrong type. In these cases a `type: ignore[unreachable]` is needed, but I think it's a nice code hint. --- setup.cfg | 1 + src/_pytest/_code/code.py | 2 +- src/_pytest/assertion/__init__.py | 2 +- src/_pytest/assertion/rewrite.py | 8 +++----- src/_pytest/capture.py | 2 +- src/_pytest/compat.py | 5 +++-- src/_pytest/config/__init__.py | 2 +- src/_pytest/config/findpaths.py | 2 +- src/_pytest/debugging.py | 25 ++++++++++++++++--------- src/_pytest/doctest.py | 4 ++-- src/_pytest/fixtures.py | 5 +++-- src/_pytest/junitxml.py | 2 +- src/_pytest/monkeypatch.py | 4 ++-- src/_pytest/outcomes.py | 2 +- src/_pytest/pathlib.py | 1 - src/_pytest/python.py | 5 ++++- src/_pytest/python_api.py | 7 ++++--- src/_pytest/reports.py | 3 ++- src/_pytest/terminal.py | 8 ++++---- src/_pytest/tmpdir.py | 2 +- testing/code/test_source.py | 2 +- testing/test_assertrewrite.py | 6 +++--- testing/test_monkeypatch.py | 2 +- testing/test_pytester.py | 4 +++- 24 files changed, 60 insertions(+), 46 deletions(-) diff --git a/setup.cfg b/setup.cfg index 9a4d841e9a7..f4170f15ae2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -103,5 +103,6 @@ show_error_codes = True strict_equality = True warn_redundant_casts = True warn_return_any = True +warn_unreachable = True warn_unused_configs = True no_implicit_reexport = True diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 4461fbfc99e..b2e4fcd33d9 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -676,7 +676,7 @@ def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]: def get_source( self, - source: "Source", + source: Optional["Source"], line_index: int = -1, excinfo: Optional[ExceptionInfo[BaseException]] = None, short: bool = False, diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index bf9dadf4b00..06057d0c4c1 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -57,7 +57,7 @@ def register_assert_rewrite(*names: str) -> None: """ for name in names: if not isinstance(name, str): - msg = "expected module names as *args, got {0} instead" + msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable] raise TypeError(msg.format(repr(names))) for hook in sys.meta_path: if isinstance(hook, rewrite.AssertionRewritingHook): diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ec3669a2e52..50fa4d405a2 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -16,6 +16,7 @@ from typing import Callable from typing import Dict from typing import IO +from typing import Iterable from typing import List from typing import Optional from typing import Sequence @@ -455,12 +456,9 @@ def _should_repr_global_name(obj: object) -> bool: return True -def _format_boolop(explanations, is_or: bool): +def _format_boolop(explanations: Iterable[str], is_or: bool) -> str: explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")" - if isinstance(explanation, str): - return explanation.replace("%", "%%") - else: - return explanation.replace(b"%", b"%%") + return explanation.replace("%", "%%") def _call_reprcompare( diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 90f5f9f3f11..99a587fcb7d 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -116,7 +116,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: return # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666). - if not hasattr(stream, "buffer"): + if not hasattr(stream, "buffer"): # type: ignore[unreachable] return buffered = hasattr(stream.buffer, "raw") diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 93232f1bfbb..ff98492dcff 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -176,8 +176,9 @@ def getfuncargnames( p.name for p in parameters.values() if ( - p.kind is Parameter.POSITIONAL_OR_KEYWORD - or p.kind is Parameter.KEYWORD_ONLY + # TODO: Remove type ignore after https://github.com/python/typeshed/pull/4383 + p.kind is Parameter.POSITIONAL_OR_KEYWORD # type: ignore[unreachable] + or p.kind is Parameter.KEYWORD_ONLY # type: ignore[unreachable] ) and p.default is Parameter.empty ) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index e0c463d2f4c..455a14b4040 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1390,7 +1390,7 @@ def _assertion_supported() -> bool: except AssertionError: return True else: - return False + return False # type: ignore[unreachable] def create_terminal_writer( diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index dcd0be9ed15..be25fc82920 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -107,7 +107,7 @@ def locate_config( def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local: - common_ancestor = None + common_ancestor = None # type: Optional[py.path.local] for path in paths: if not path.exists(): continue diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 69e6b4dd4a6..6f641fb2d96 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -7,6 +7,7 @@ from typing import Callable from typing import Generator from typing import List +from typing import Optional from typing import Tuple from typing import Union @@ -23,6 +24,8 @@ from _pytest.reports import BaseReport if TYPE_CHECKING: + from typing import Type + from _pytest.capture import CaptureManager from _pytest.runner import CallInfo @@ -92,20 +95,22 @@ def fin() -> None: class pytestPDB: """Pseudo PDB that defers to the real pdb.""" - _pluginmanager = None # type: PytestPluginManager + _pluginmanager = None # type: Optional[PytestPluginManager] _config = None # type: Config - _saved = [] # type: List[Tuple[Callable[..., None], PytestPluginManager, Config]] + _saved = ( + [] + ) # type: List[Tuple[Callable[..., None], Optional[PytestPluginManager], Config]] _recursive_debug = 0 - _wrapped_pdb_cls = None + _wrapped_pdb_cls = None # type: Optional[Tuple[Type[Any], Type[Any]]] @classmethod - def _is_capturing(cls, capman: "CaptureManager") -> Union[str, bool]: + def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]: if capman: return capman.is_capturing() return False @classmethod - def _import_pdb_cls(cls, capman: "CaptureManager"): + def _import_pdb_cls(cls, capman: Optional["CaptureManager"]): if not cls._config: import pdb @@ -144,7 +149,7 @@ def _import_pdb_cls(cls, capman: "CaptureManager"): return wrapped_cls @classmethod - def _get_pdb_wrapper_class(cls, pdb_cls, capman: "CaptureManager"): + def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]): import _pytest.config # Type ignored because mypy doesn't support "dynamic" @@ -176,9 +181,11 @@ def do_continue(self, arg): "PDB continue (IO-capturing resumed for %s)" % capturing, ) + assert capman is not None capman.resume() else: tw.sep(">", "PDB continue") + assert cls._pluginmanager is not None cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self) self._continued = True return ret @@ -232,10 +239,10 @@ def _init_pdb(cls, method, *args, **kwargs): """Initialize PDB debugging, dropping any IO capturing.""" import _pytest.config - if cls._pluginmanager is not None: - capman = cls._pluginmanager.getplugin("capturemanager") + if cls._pluginmanager is None: + capman = None # type: Optional[CaptureManager] else: - capman = None + capman = cls._pluginmanager.getplugin("capturemanager") if capman: capman.suspend(in_=True) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 440bc649c1e..acedd389b32 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -635,8 +635,8 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: return got offset = 0 for w, g in zip(wants, gots): - fraction = w.group("fraction") - exponent = w.group("exponent1") + fraction = w.group("fraction") # type: Optional[str] + exponent = w.group("exponent1") # type: Optional[str] if exponent is None: exponent = w.group("exponent2") if fraction is None: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 6510985219a..d2ff6203b4b 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -993,7 +993,8 @@ def __init__( else: scope_ = scope self.scopenum = scope2index( - scope_ or "function", + # TODO: Check if the `or` here is really necessary. + scope_ or "function", # type: ignore[unreachable] descr="Fixture '{}'".format(func.__name__), where=baseid, ) @@ -1319,7 +1320,7 @@ def fixture( # noqa: F811 # **kwargs and check `in`, but that obfuscates the function signature. if isinstance(fixture_function, str): # It's actually the first positional argument, scope. - args = (fixture_function, *args) + args = (fixture_function, *args) # type: ignore[unreachable] fixture_function = None duplicated_args = [] if len(args) > 0: diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 6e3785b7d6f..8ecc13a8c54 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -330,7 +330,7 @@ def _check_record_param_type(param: str, v: str) -> None: type.""" __tracebackhide__ = True if not isinstance(v, str): - msg = "{param} parameter needs to be a string, but {g} given" + msg = "{param} parameter needs to be a string, but {g} given" # type: ignore[unreachable] raise TypeError(msg.format(param=param, g=type(v).__name__)) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 19208ac6630..4a4dd67a135 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -91,7 +91,7 @@ def annotated_getattr(obj: object, name: str, ann: str) -> object: def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]: - if not isinstance(import_path, str) or "." not in import_path: + if not isinstance(import_path, str) or "." not in import_path: # type: ignore[unreachable] raise TypeError( "must be absolute import path string, not {!r}".format(import_path) ) @@ -272,7 +272,7 @@ def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: character. """ if not isinstance(value, str): - warnings.warn( + warnings.warn( # type: ignore[unreachable] pytest.PytestWarning( "Value of environment variable {name} type should be str, but got " "{value!r} (type: {type}); converted to str implicitly".format( diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 3ce026b89b3..a2ddc3a1f1a 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -28,7 +28,7 @@ class OutcomeException(BaseException): def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None: if msg is not None and not isinstance(msg, str): - error_msg = ( + error_msg = ( # type: ignore[unreachable] "{} expected string as 'msg' parameter, got '{}' instead.\n" "Perhaps you meant to use a mark?" ) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index ea263be7009..b3129020dee 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -367,7 +367,6 @@ def make_numbered_dir_with_cleanup( def resolve_from_str(input: str, root: py.path.local) -> Path: - assert not isinstance(input, Path), "would break on py2" rootpath = Path(root) input = expanduser(input) input = expandvars(input) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index d942b4fa690..7416245653d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1097,7 +1097,10 @@ def _validate_ids( elif isinstance(id_value, (float, int, bool)): new_ids.append(str(id_value)) else: - msg = "In {}: ids must be list of string/float/int/bool, found: {} (type: {!r}) at index {}" + msg = ( # type: ignore[unreachable] + "In {}: ids must be list of string/float/int/bool, " + "found: {} (type: {!r}) at index {}" + ) fail( msg.format(func_name, saferepr(id_value), type(id_value), idx), pytrace=False, diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index b003db72abd..c0c266cbd18 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -495,7 +495,8 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: elif ( isinstance(expected, Iterable) and isinstance(expected, Sized) - and not isinstance(expected, STRING_TYPES) + # Type ignored because the error is wrong -- not unreachable. + and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable] ): cls = ApproxSequencelike else: @@ -662,8 +663,8 @@ def raises( # noqa: F811 else: excepted_exceptions = expected_exception for exc in excepted_exceptions: - if not isinstance(exc, type) or not issubclass(exc, BaseException): - msg = "expected exception must be a BaseException type, not {}" + if not isinstance(exc, type) or not issubclass(exc, BaseException): # type: ignore[unreachable] + msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable] not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__ raise TypeError(msg.format(not_a)) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 8461ad663e6..707972fcf8f 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -386,7 +386,8 @@ def pytest_report_to_serializable( data = report._to_json() data["$report_type"] = report.__class__.__name__ return data - return None + # TODO: Check if this is actually reachable. + return None # type: ignore[unreachable] def pytest_report_from_serializable( diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index cb58f559546..e6ba293e610 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -522,12 +522,12 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: rep = report res = self.config.hook.pytest_report_teststatus( report=rep, config=self.config - ) # type: Tuple[str, str, str] + ) # type: Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]] category, letter, word = res - if isinstance(word, tuple): - word, markup = word - else: + if not isinstance(word, tuple): markup = None + else: + word, markup = word self._add_stats(category, [rep]) if not letter and not word: # Probably passed setup/teardown. diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 017577a7ac8..f1a8f1e9f11 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -26,7 +26,7 @@ class TempPathFactory: """ _given_basetemp = attr.ib( - type=Path, + type=Optional[Path], # Use os.path.abspath() to get absolute path instead of resolve() as it # does not work the same in all platforms (see #4427). # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012). diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 4222eb172f2..d12c55d935b 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -238,7 +238,7 @@ def c() -> None: c(1) # type: ignore finally: if teardown: - teardown() + teardown() # type: ignore[unreachable] source = excinfo.traceback[-1].statement assert str(source).strip() == "c(1) # type: ignore" diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 23f535173c2..63a6fdd1285 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -381,7 +381,7 @@ def f6() -> None: ) def f7() -> None: - assert False or x() + assert False or x() # type: ignore[unreachable] assert ( getmsg(f7, {"x": x}) @@ -416,7 +416,7 @@ def f11() -> None: def test_short_circuit_evaluation(self) -> None: def f1() -> None: - assert True or explode # type: ignore[name-defined] # noqa: F821 + assert True or explode # type: ignore[name-defined,unreachable] # noqa: F821 getmsg(f1, must_pass=True) @@ -471,7 +471,7 @@ def f1() -> None: assert getmsg(f1) == "assert ((3 % 2) and False)" def f2() -> None: - assert False or 4 % 2 + assert False or 4 % 2 # type: ignore[unreachable] assert getmsg(f2) == "assert (False or (4 % 2))" diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 509e72599c7..fea8a28fba8 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -360,7 +360,7 @@ def test_issue156_undo_staticmethod(Sample: "Type[Sample]") -> None: monkeypatch.setattr(Sample, "hello", None) assert Sample.hello is None - monkeypatch.undo() + monkeypatch.undo() # type: ignore[unreachable] assert Sample.hello() diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 5259b448418..46fab0ce893 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -23,7 +23,9 @@ def test_make_hook_recorder(testdir) -> None: recorder = testdir.make_hook_recorder(item.config.pluginmanager) assert not recorder.getfailures() - pytest.xfail("internal reportrecorder tests need refactoring") + # (The silly condition is to fool mypy that the code below this is reachable) + if 1 + 1 == 2: + pytest.xfail("internal reportrecorder tests need refactoring") class rep: excinfo = None From 84c4b643547b9fc09c8a2715a2b906d7a1844195 Mon Sep 17 00:00:00 2001 From: Yutaro Ikeda Date: Wed, 5 Aug 2020 03:30:08 +0900 Subject: [PATCH 541/823] Better document -k partial matching (#7610) --- doc/en/example/markers.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 99e3386b8b5..3d55f9ebb04 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -201,6 +201,11 @@ Or to select "http" and "quick" tests: You can use ``and``, ``or``, ``not`` and parentheses. +In addition to the test's name, ``-k`` also matches the names of the test's parents (usually, the name of the file and class it's in), +attributes set on the test function, markers applied to it or its parents and any :attr:`extra keywords <_pytest.nodes.Node.extra_keyword_matches>` +explicitly added to it or its parents. + + Registering markers ------------------------------------- From 62ddf7a0e589e19355f90e4e0412fd771233a62a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 1 Aug 2020 10:11:24 +0300 Subject: [PATCH 542/823] resultlog: add missing type annotations --- src/_pytest/resultlog.py | 12 ++++++++---- testing/test_resultlog.py | 32 ++++++++++++++++++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index 356a39c1217..88216d6845a 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -1,5 +1,7 @@ """log machine-parseable test session result information to a plain text file.""" import os +from typing import IO +from typing import Union import py @@ -52,16 +54,18 @@ def pytest_unconfigure(config: Config) -> None: class ResultLog: - def __init__(self, config, logfile): + def __init__(self, config: Config, logfile: IO[str]) -> None: self.config = config self.logfile = logfile # preferably line buffered - def write_log_entry(self, testpath, lettercode, longrepr): + def write_log_entry(self, testpath: str, lettercode: str, longrepr: str) -> None: print("{} {}".format(lettercode, testpath), file=self.logfile) for line in longrepr.splitlines(): print(" %s" % line, file=self.logfile) - def log_outcome(self, report, lettercode, longrepr): + def log_outcome( + self, report: Union[TestReport, CollectReport], lettercode: str, longrepr: str + ) -> None: testpath = getattr(report, "nodeid", None) if testpath is None: testpath = report.fspath @@ -73,7 +77,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: res = self.config.hook.pytest_report_teststatus( report=report, config=self.config ) - code = res[1] + code = res[1] # type: str if code == "x": longrepr = str(report.longrepr) elif code == "X": diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index 8fc93d25c7d..f2eb612c1e7 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -1,18 +1,21 @@ import os from io import StringIO +from typing import List import _pytest._code import pytest +from _pytest.pytester import Testdir from _pytest.resultlog import pytest_configure from _pytest.resultlog import pytest_unconfigure from _pytest.resultlog import ResultLog from _pytest.resultlog import resultlog_key + pytestmark = pytest.mark.filterwarnings("ignore:--result-log is deprecated") -def test_write_log_entry(): - reslog = ResultLog(None, None) +def test_write_log_entry() -> None: + reslog = ResultLog(None, None) # type: ignore[arg-type] reslog.logfile = StringIO() reslog.write_log_entry("name", ".", "") entry = reslog.logfile.getvalue() @@ -54,14 +57,14 @@ class TestWithFunctionIntegration: # XXX (hpk) i think that the resultlog plugin should # provide a Parser object so that one can remain # ignorant regarding formatting details. - def getresultlog(self, testdir, arg): + def getresultlog(self, testdir: Testdir, arg: str) -> List[str]: resultlog = testdir.tmpdir.join("resultlog") testdir.plugins.append("resultlog") args = ["--resultlog=%s" % resultlog] + [arg] testdir.runpytest(*args) return [x for x in resultlog.readlines(cr=0) if x] - def test_collection_report(self, testdir): + def test_collection_report(self, testdir: Testdir) -> None: ok = testdir.makepyfile(test_collection_ok="") fail = testdir.makepyfile(test_collection_fail="XXX") lines = self.getresultlog(testdir, ok) @@ -75,7 +78,7 @@ def test_collection_report(self, testdir): assert x.startswith(" ") assert "XXX" in "".join(lines[1:]) - def test_log_test_outcomes(self, testdir): + def test_log_test_outcomes(self, testdir: Testdir) -> None: mod = testdir.makepyfile( test_mod=""" import pytest @@ -111,16 +114,17 @@ def test_xpass(): pass assert len(lines) == 15 @pytest.mark.parametrize("style", ("native", "long", "short")) - def test_internal_exception(self, style): + def test_internal_exception(self, style) -> None: # they are produced for example by a teardown failing # at the end of the run or a failing hook invocation try: raise ValueError except ValueError: excinfo = _pytest._code.ExceptionInfo.from_current() - reslog = ResultLog(None, StringIO()) + file = StringIO() + reslog = ResultLog(None, file) # type: ignore[arg-type] reslog.pytest_internalerror(excinfo.getrepr(style=style)) - entry = reslog.logfile.getvalue() + entry = file.getvalue() entry_lines = entry.splitlines() assert entry_lines[0].startswith("! ") @@ -130,7 +134,7 @@ def test_internal_exception(self, style): assert "ValueError" in entry -def test_generic(testdir, LineMatcher): +def test_generic(testdir: Testdir, LineMatcher) -> None: testdir.plugins.append("resultlog") testdir.makepyfile( """ @@ -162,7 +166,7 @@ def test_xfail_norun(): ) -def test_makedir_for_resultlog(testdir, LineMatcher): +def test_makedir_for_resultlog(testdir: Testdir, LineMatcher) -> None: """--resultlog should automatically create directories for the log file""" testdir.plugins.append("resultlog") testdir.makepyfile( @@ -177,7 +181,7 @@ def test_pass(): LineMatcher(lines).fnmatch_lines([". *:test_pass"]) -def test_no_resultlog_on_workers(testdir): +def test_no_resultlog_on_workers(testdir: Testdir) -> None: config = testdir.parseconfig("-p", "resultlog", "--resultlog=resultlog") assert resultlog_key not in config._store @@ -186,14 +190,14 @@ def test_no_resultlog_on_workers(testdir): pytest_unconfigure(config) assert resultlog_key not in config._store - config.workerinput = {} + config.workerinput = {} # type: ignore[attr-defined] pytest_configure(config) assert resultlog_key not in config._store pytest_unconfigure(config) assert resultlog_key not in config._store -def test_unknown_teststatus(testdir): +def test_unknown_teststatus(testdir: Testdir) -> None: """Ensure resultlog correctly handles unknown status from pytest_report_teststatus Inspired on pytest-rerunfailures. @@ -229,7 +233,7 @@ def pytest_runtest_makereport(): assert lines[0] == "r test_unknown_teststatus.py::test" -def test_failure_issue380(testdir): +def test_failure_issue380(testdir: Testdir) -> None: testdir.makeconftest( """ import pytest From f0eb82f7d40281c28b94239e85b95918d1d7aeb9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 1 Aug 2020 10:49:51 +0300 Subject: [PATCH 543/823] pytester: improve type annotations --- src/_pytest/pytester.py | 111 +++++++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 25 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index e0e7b1fbc4a..83c525fd8c1 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -28,6 +28,7 @@ from _pytest import timing from _pytest._code import Source from _pytest.capture import _get_multicapture +from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin from _pytest.config import Config @@ -42,11 +43,13 @@ from _pytest.pathlib import make_numbered_dir from _pytest.pathlib import Path from _pytest.python import Module +from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.tmpdir import TempdirFactory if TYPE_CHECKING: from typing import Type + from typing_extensions import Literal import pexpect @@ -180,24 +183,24 @@ def gethookrecorder(self, hook) -> "HookRecorder": return hookrecorder -def get_public_names(values): +def get_public_names(values: Iterable[str]) -> List[str]: """Only return names from iterator values without a leading underscore.""" return [x for x in values if x[0] != "_"] class ParsedCall: - def __init__(self, name, kwargs): + def __init__(self, name: str, kwargs) -> None: self.__dict__.update(kwargs) self._name = name - def __repr__(self): + def __repr__(self) -> str: d = self.__dict__.copy() del d["_name"] return "".format(self._name, d) if TYPE_CHECKING: # The class has undetermined attributes, this tells mypy about it. - def __getattr__(self, key): + def __getattr__(self, key: str): raise NotImplementedError() @@ -211,6 +214,7 @@ class HookRecorder: def __init__(self, pluginmanager: PytestPluginManager) -> None: self._pluginmanager = pluginmanager self.calls = [] # type: List[ParsedCall] + self.ret = None # type: Optional[Union[int, ExitCode]] def before(hook_name: str, hook_impls, kwargs) -> None: self.calls.append(ParsedCall(hook_name, kwargs)) @@ -228,7 +232,7 @@ def getcalls(self, names: Union[str, Iterable[str]]) -> List[ParsedCall]: names = names.split() return [call for call in self.calls if call._name in names] - def assert_contains(self, entries) -> None: + def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None: __tracebackhide__ = True i = 0 entries = list(entries) @@ -266,22 +270,46 @@ def getcall(self, name: str) -> ParsedCall: # functionality for test reports + @overload def getreports( + self, names: "Literal['pytest_collectreport']", + ) -> Sequence[CollectReport]: + raise NotImplementedError() + + @overload # noqa: F811 + def getreports( # noqa: F811 + self, names: "Literal['pytest_runtest_logreport']", + ) -> Sequence[TestReport]: + raise NotImplementedError() + + @overload # noqa: F811 + def getreports( # noqa: F811 + self, + names: Union[str, Iterable[str]] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[Union[CollectReport, TestReport]]: + raise NotImplementedError() + + def getreports( # noqa: F811 self, - names: Union[ - str, Iterable[str] - ] = "pytest_runtest_logreport pytest_collectreport", - ) -> List[TestReport]: + names: Union[str, Iterable[str]] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[Union[CollectReport, TestReport]]: return [x.report for x in self.getcalls(names)] def matchreport( self, inamepart: str = "", - names: Union[ - str, Iterable[str] - ] = "pytest_runtest_logreport pytest_collectreport", - when=None, - ): + names: Union[str, Iterable[str]] = ( + "pytest_runtest_logreport", + "pytest_collectreport", + ), + when: Optional[str] = None, + ) -> Union[CollectReport, TestReport]: """Return a testreport whose dotted import path matches.""" values = [] for rep in self.getreports(names=names): @@ -305,26 +333,56 @@ def matchreport( ) return values[0] + @overload def getfailures( + self, names: "Literal['pytest_collectreport']", + ) -> Sequence[CollectReport]: + raise NotImplementedError() + + @overload # noqa: F811 + def getfailures( # noqa: F811 + self, names: "Literal['pytest_runtest_logreport']", + ) -> Sequence[TestReport]: + raise NotImplementedError() + + @overload # noqa: F811 + def getfailures( # noqa: F811 self, - names: Union[ - str, Iterable[str] - ] = "pytest_runtest_logreport pytest_collectreport", - ) -> List[TestReport]: + names: Union[str, Iterable[str]] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[Union[CollectReport, TestReport]]: + raise NotImplementedError() + + def getfailures( # noqa: F811 + self, + names: Union[str, Iterable[str]] = ( + "pytest_collectreport", + "pytest_runtest_logreport", + ), + ) -> Sequence[Union[CollectReport, TestReport]]: return [rep for rep in self.getreports(names) if rep.failed] - def getfailedcollections(self) -> List[TestReport]: + def getfailedcollections(self) -> Sequence[CollectReport]: return self.getfailures("pytest_collectreport") def listoutcomes( self, - ) -> Tuple[List[TestReport], List[TestReport], List[TestReport]]: + ) -> Tuple[ + Sequence[TestReport], + Sequence[Union[CollectReport, TestReport]], + Sequence[Union[CollectReport, TestReport]], + ]: passed = [] skipped = [] failed = [] - for rep in self.getreports("pytest_collectreport pytest_runtest_logreport"): + for rep in self.getreports( + ("pytest_collectreport", "pytest_runtest_logreport") + ): if rep.passed: if rep.when == "call": + assert isinstance(rep, TestReport) passed.append(rep) elif rep.skipped: skipped.append(rep) @@ -879,7 +937,7 @@ def runitem(self, source): runner = testclassinstance.getrunner() return runner(item) - def inline_runsource(self, source, *cmdlineargs): + def inline_runsource(self, source, *cmdlineargs) -> HookRecorder: """Run a test module in process using ``pytest.main()``. This run writes "source" into a temporary file and runs @@ -896,7 +954,7 @@ def inline_runsource(self, source, *cmdlineargs): values = list(cmdlineargs) + [p] return self.inline_run(*values) - def inline_genitems(self, *args): + def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]: """Run ``pytest.main(['--collectonly'])`` in-process. Runs the :py:func:`pytest.main` function to run all of pytest inside @@ -907,7 +965,9 @@ def inline_genitems(self, *args): items = [x.item for x in rec.getcalls("pytest_itemcollected")] return items, rec - def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): + def inline_run( + self, *args, plugins=(), no_reraise_ctrlc: bool = False + ) -> HookRecorder: """Run ``pytest.main()`` in-process, returning a HookRecorder. Runs the :py:func:`pytest.main` function to run all of pytest inside @@ -962,7 +1022,7 @@ def pytest_configure(x, config: Config) -> None: class reprec: # type: ignore pass - reprec.ret = ret # type: ignore[attr-defined] + reprec.ret = ret # Typically we reraise keyboard interrupts from the child run # because it's our user requesting interruption of the testing. @@ -1010,6 +1070,7 @@ class reprec: # type: ignore sys.stdout.write(out) sys.stderr.write(err) + assert reprec.ret is not None res = RunResult( reprec.ret, out.splitlines(), err.splitlines(), timing.time() - now ) From fbf251f11d22fe35fc9aa48bdd04b6625e9d9123 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 1 Aug 2020 10:28:39 +0300 Subject: [PATCH 544/823] Improve typing of reports' longrepr field --- src/_pytest/junitxml.py | 17 +++++-- src/_pytest/reports.py | 37 ++++++++------ src/_pytest/resultlog.py | 2 +- src/_pytest/runner.py | 6 +-- src/_pytest/terminal.py | 1 + testing/test_reports.py | 101 +++++++++++++++++++++++++-------------- 6 files changed, 104 insertions(+), 60 deletions(-) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 8ecc13a8c54..1e563eb8d25 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -25,6 +25,7 @@ from _pytest import nodes from _pytest import timing from _pytest._code.code import ExceptionRepr +from _pytest._code.code import ReprFileLocation from _pytest.config import Config from _pytest.config import filename_arg from _pytest.config.argparsing import Parser @@ -200,8 +201,11 @@ def append_failure(self, report: TestReport) -> None: self._add_simple("skipped", "xfail-marked test passes unexpectedly") else: assert report.longrepr is not None - if getattr(report.longrepr, "reprcrash", None) is not None: - message = report.longrepr.reprcrash.message + reprcrash = getattr( + report.longrepr, "reprcrash", None + ) # type: Optional[ReprFileLocation] + if reprcrash is not None: + message = reprcrash.message else: message = str(report.longrepr) message = bin_xml_escape(message) @@ -217,8 +221,11 @@ def append_collect_skipped(self, report: TestReport) -> None: def append_error(self, report: TestReport) -> None: assert report.longrepr is not None - if getattr(report.longrepr, "reprcrash", None) is not None: - reason = report.longrepr.reprcrash.message + reprcrash = getattr( + report.longrepr, "reprcrash", None + ) # type: Optional[ReprFileLocation] + if reprcrash is not None: + reason = reprcrash.message else: reason = str(report.longrepr) @@ -237,7 +244,7 @@ def append_skipped(self, report: TestReport) -> None: skipped = ET.Element("skipped", type="pytest.xfail", message=xfailreason) self.append(skipped) else: - assert report.longrepr is not None + assert isinstance(report.longrepr, tuple) filename, lineno, skipreason = report.longrepr if skipreason.startswith("Skipped: "): skipreason = skipreason[9:] diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 707972fcf8f..3053798634c 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,6 +1,7 @@ from io import StringIO from pprint import pprint from typing import Any +from typing import cast from typing import Dict from typing import Iterable from typing import Iterator @@ -15,6 +16,7 @@ from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo +from _pytest._code.code import ExceptionRepr from _pytest._code.code import ReprEntry from _pytest._code.code import ReprEntryNative from _pytest._code.code import ReprExceptionInfo @@ -57,8 +59,9 @@ def getworkerinfoline(node): class BaseReport: when = None # type: Optional[str] location = None # type: Optional[Tuple[str, Optional[int], str]] - # TODO: Improve this Any. - longrepr = None # type: Optional[Any] + longrepr = ( + None + ) # type: Union[None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr] sections = [] # type: List[Tuple[str, str]] nodeid = None # type: str @@ -79,7 +82,8 @@ def toterminal(self, out: TerminalWriter) -> None: return if hasattr(longrepr, "toterminal"): - longrepr.toterminal(out) + longrepr_terminal = cast(TerminalRepr, longrepr) + longrepr_terminal.toterminal(out) else: try: s = str(longrepr) @@ -233,7 +237,9 @@ def __init__( location: Tuple[str, Optional[int], str], keywords, outcome: "Literal['passed', 'failed', 'skipped']", - longrepr, + longrepr: Union[ + None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr + ], when: "Literal['setup', 'call', 'teardown']", sections: Iterable[Tuple[str, str]] = (), duration: float = 0, @@ -293,8 +299,9 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": sections = [] if not call.excinfo: outcome = "passed" # type: Literal["passed", "failed", "skipped"] - # TODO: Improve this Any. - longrepr = None # type: Optional[Any] + longrepr = ( + None + ) # type: Union[None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr] else: if not isinstance(excinfo, ExceptionInfo): outcome = "failed" @@ -372,7 +379,7 @@ def __repr__(self) -> str: class CollectErrorRepr(TerminalRepr): - def __init__(self, msg) -> None: + def __init__(self, msg: str) -> None: self.longrepr = msg def toterminal(self, out: TerminalWriter) -> None: @@ -436,16 +443,18 @@ def serialize_repr_crash( else: return None - def serialize_longrepr(rep: BaseReport) -> Dict[str, Any]: + def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]: assert rep.longrepr is not None + # TODO: Investigate whether the duck typing is really necessary here. + longrepr = cast(ExceptionRepr, rep.longrepr) result = { - "reprcrash": serialize_repr_crash(rep.longrepr.reprcrash), - "reprtraceback": serialize_repr_traceback(rep.longrepr.reprtraceback), - "sections": rep.longrepr.sections, + "reprcrash": serialize_repr_crash(longrepr.reprcrash), + "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback), + "sections": longrepr.sections, } # type: Dict[str, Any] - if isinstance(rep.longrepr, ExceptionChainRepr): + if isinstance(longrepr, ExceptionChainRepr): result["chain"] = [] - for repr_traceback, repr_crash, description in rep.longrepr.chain: + for repr_traceback, repr_crash, description in longrepr.chain: result["chain"].append( ( serialize_repr_traceback(repr_traceback), @@ -462,7 +471,7 @@ def serialize_longrepr(rep: BaseReport) -> Dict[str, Any]: if hasattr(report.longrepr, "reprtraceback") and hasattr( report.longrepr, "reprcrash" ): - d["longrepr"] = serialize_longrepr(report) + d["longrepr"] = serialize_exception_longrepr(report) else: d["longrepr"] = str(report.longrepr) else: diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index 88216d6845a..c043c749f87 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -85,7 +85,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: elif report.passed: longrepr = "" elif report.skipped: - assert report.longrepr is not None + assert isinstance(report.longrepr, tuple) longrepr = str(report.longrepr[2]) else: longrepr = str(report.longrepr) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 4923406b9ba..4089fc689fd 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -2,7 +2,6 @@ import bdb import os import sys -from typing import Any from typing import Callable from typing import cast from typing import Dict @@ -22,6 +21,7 @@ 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 TYPE_CHECKING from _pytest.config.argparsing import Parser from _pytest.nodes import Collector @@ -327,8 +327,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: def pytest_make_collect_report(collector: Collector) -> CollectReport: call = CallInfo.from_call(lambda: list(collector.collect()), "collect") - # TODO: Better typing for longrepr. - longrepr = None # type: Optional[Any] + longrepr = None # type: Union[None, Tuple[str, int, str], str, TerminalRepr] if not call.excinfo: outcome = "passed" # type: Literal["passed", "skipped", "failed"] else: @@ -348,6 +347,7 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: outcome = "failed" errorinfo = collector.repr_failure(call.excinfo) if not hasattr(errorinfo, "toterminal"): + assert isinstance(errorinfo, str) errorinfo = CollectErrorRepr(errorinfo) longrepr = errorinfo result = call.result if not call.excinfo else None diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index e6ba293e610..1d49df4cf89 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1247,6 +1247,7 @@ def _folded_skips( d = {} # type: Dict[Tuple[str, Optional[int], str], List[CollectReport]] for event in skipped: assert event.longrepr is not None + assert isinstance(event.longrepr, tuple), (event, event.longrepr) assert len(event.longrepr) == 3, (event, event.longrepr) fspath, lineno, reason = event.longrepr # For consistency, report all fspaths in relative form. diff --git a/testing/test_reports.py b/testing/test_reports.py index a47d5378705..dbe94896207 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -1,14 +1,19 @@ import sys +from typing import Sequence +from typing import Union import pytest from _pytest._code.code import ExceptionChainRepr +from _pytest._code.code import ExceptionRepr +from _pytest.config import Config from _pytest.pathlib import Path +from _pytest.pytester import Testdir from _pytest.reports import CollectReport from _pytest.reports import TestReport class TestReportSerialization: - def test_xdist_longrepr_to_str_issue_241(self, testdir): + def test_xdist_longrepr_to_str_issue_241(self, testdir: Testdir) -> None: """Regarding issue pytest-xdist#241. This test came originally from test_remote.py in xdist (ca03269). @@ -31,7 +36,7 @@ def test_b(): pass assert test_b_call.outcome == "passed" assert test_b_call._to_json()["longrepr"] is None - def test_xdist_report_longrepr_reprcrash_130(self, testdir) -> None: + def test_xdist_report_longrepr_reprcrash_130(self, testdir: Testdir) -> None: """Regarding issue pytest-xdist#130 This test came originally from test_remote.py in xdist (ca03269). @@ -46,15 +51,18 @@ def test_fail(): assert len(reports) == 3 rep = reports[1] added_section = ("Failure Metadata", "metadata metadata", "*") + assert isinstance(rep.longrepr, ExceptionRepr) rep.longrepr.sections.append(added_section) d = rep._to_json() a = TestReport._from_json(d) - assert a.longrepr is not None + assert isinstance(a.longrepr, ExceptionRepr) # Check assembled == rep assert a.__dict__.keys() == rep.__dict__.keys() for key in rep.__dict__.keys(): if key != "longrepr": assert getattr(a, key) == getattr(rep, key) + assert rep.longrepr.reprcrash is not None + assert a.longrepr.reprcrash is not None assert rep.longrepr.reprcrash.lineno == a.longrepr.reprcrash.lineno assert rep.longrepr.reprcrash.message == a.longrepr.reprcrash.message assert rep.longrepr.reprcrash.path == a.longrepr.reprcrash.path @@ -67,7 +75,7 @@ def test_fail(): # Missing section attribute PR171 assert added_section in a.longrepr.sections - def test_reprentries_serialization_170(self, testdir) -> None: + def test_reprentries_serialization_170(self, testdir: Testdir) -> None: """Regarding issue pytest-xdist#170 This test came originally from test_remote.py in xdist (ca03269). @@ -85,25 +93,35 @@ def test_repr_entry(): reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 3 rep = reports[1] + assert isinstance(rep.longrepr, ExceptionRepr) d = rep._to_json() a = TestReport._from_json(d) - assert a.longrepr is not None + assert isinstance(a.longrepr, ExceptionRepr) rep_entries = rep.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries for i in range(len(a_entries)): - assert isinstance(rep_entries[i], ReprEntry) - assert rep_entries[i].lines == a_entries[i].lines - assert rep_entries[i].reprfileloc.lineno == a_entries[i].reprfileloc.lineno - assert ( - rep_entries[i].reprfileloc.message == a_entries[i].reprfileloc.message - ) - assert rep_entries[i].reprfileloc.path == a_entries[i].reprfileloc.path - assert rep_entries[i].reprfuncargs.args == a_entries[i].reprfuncargs.args - assert rep_entries[i].reprlocals.lines == a_entries[i].reprlocals.lines - assert rep_entries[i].style == a_entries[i].style - - def test_reprentries_serialization_196(self, testdir) -> None: + rep_entry = rep_entries[i] + assert isinstance(rep_entry, ReprEntry) + assert rep_entry.reprfileloc is not None + assert rep_entry.reprfuncargs is not None + assert rep_entry.reprlocals is not None + + a_entry = a_entries[i] + assert isinstance(a_entry, ReprEntry) + assert a_entry.reprfileloc is not None + assert a_entry.reprfuncargs is not None + assert a_entry.reprlocals is not None + + assert rep_entry.lines == a_entry.lines + assert rep_entry.reprfileloc.lineno == a_entry.reprfileloc.lineno + assert rep_entry.reprfileloc.message == a_entry.reprfileloc.message + assert rep_entry.reprfileloc.path == a_entry.reprfileloc.path + assert rep_entry.reprfuncargs.args == a_entry.reprfuncargs.args + assert rep_entry.reprlocals.lines == a_entry.reprlocals.lines + assert rep_entry.style == a_entry.style + + def test_reprentries_serialization_196(self, testdir: Testdir) -> None: """Regarding issue pytest-xdist#196 This test came originally from test_remote.py in xdist (ca03269). @@ -121,9 +139,10 @@ def test_repr_entry_native(): reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 3 rep = reports[1] + assert isinstance(rep.longrepr, ExceptionRepr) d = rep._to_json() a = TestReport._from_json(d) - assert a.longrepr is not None + assert isinstance(a.longrepr, ExceptionRepr) rep_entries = rep.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries @@ -131,7 +150,7 @@ def test_repr_entry_native(): assert isinstance(rep_entries[i], ReprEntryNative) assert rep_entries[i].lines == a_entries[i].lines - def test_itemreport_outcomes(self, testdir): + def test_itemreport_outcomes(self, testdir: Testdir) -> None: # This test came originally from test_remote.py in xdist (ca03269). reprec = testdir.inline_runsource( """ @@ -157,7 +176,7 @@ def test_xfail_imperative(): assert newrep.failed == rep.failed assert newrep.skipped == rep.skipped if newrep.skipped and not hasattr(newrep, "wasxfail"): - assert newrep.longrepr is not None + assert isinstance(newrep.longrepr, tuple) assert len(newrep.longrepr) == 3 assert newrep.outcome == rep.outcome assert newrep.when == rep.when @@ -165,7 +184,7 @@ def test_xfail_imperative(): if rep.failed: assert newrep.longreprtext == rep.longreprtext - def test_collectreport_passed(self, testdir): + def test_collectreport_passed(self, testdir: Testdir) -> None: """This test came originally from test_remote.py in xdist (ca03269).""" reprec = testdir.inline_runsource("def test_func(): pass") reports = reprec.getreports("pytest_collectreport") @@ -176,7 +195,7 @@ def test_collectreport_passed(self, testdir): assert newrep.failed == rep.failed assert newrep.skipped == rep.skipped - def test_collectreport_fail(self, testdir): + def test_collectreport_fail(self, testdir: Testdir) -> None: """This test came originally from test_remote.py in xdist (ca03269).""" reprec = testdir.inline_runsource("qwe abc") reports = reprec.getreports("pytest_collectreport") @@ -190,13 +209,13 @@ def test_collectreport_fail(self, testdir): if rep.failed: assert newrep.longrepr == str(rep.longrepr) - def test_extended_report_deserialization(self, testdir): + def test_extended_report_deserialization(self, testdir: Testdir) -> None: """This test came originally from test_remote.py in xdist (ca03269).""" reprec = testdir.inline_runsource("qwe abc") reports = reprec.getreports("pytest_collectreport") assert reports for rep in reports: - rep.extra = True + rep.extra = True # type: ignore[attr-defined] d = rep._to_json() newrep = CollectReport._from_json(d) assert newrep.extra @@ -206,7 +225,7 @@ def test_extended_report_deserialization(self, testdir): if rep.failed: assert newrep.longrepr == str(rep.longrepr) - def test_paths_support(self, testdir): + def test_paths_support(self, testdir: Testdir) -> None: """Report attributes which are py.path or pathlib objects should become strings.""" testdir.makepyfile( """ @@ -218,13 +237,13 @@ def test_a(): reports = reprec.getreports("pytest_runtest_logreport") assert len(reports) == 3 test_a_call = reports[1] - test_a_call.path1 = testdir.tmpdir - test_a_call.path2 = Path(testdir.tmpdir) + test_a_call.path1 = testdir.tmpdir # type: ignore[attr-defined] + test_a_call.path2 = Path(testdir.tmpdir) # type: ignore[attr-defined] data = test_a_call._to_json() assert data["path1"] == str(testdir.tmpdir) assert data["path2"] == str(testdir.tmpdir) - def test_deserialization_failure(self, testdir): + def test_deserialization_failure(self, testdir: Testdir) -> None: """Check handling of failure during deserialization of report types.""" testdir.makepyfile( """ @@ -247,7 +266,7 @@ def test_a(): TestReport._from_json(data) @pytest.mark.parametrize("report_class", [TestReport, CollectReport]) - def test_chained_exceptions(self, testdir, tw_mock, report_class): + def test_chained_exceptions(self, testdir: Testdir, tw_mock, report_class) -> None: """Check serialization/deserialization of report objects containing chained exceptions (#5786)""" testdir.makepyfile( """ @@ -267,7 +286,9 @@ def test_a(): reprec = testdir.inline_run() if report_class is TestReport: - reports = reprec.getreports("pytest_runtest_logreport") + reports = reprec.getreports( + "pytest_runtest_logreport" + ) # type: Union[Sequence[TestReport], Sequence[CollectReport]] # we have 3 reports: setup/call/teardown assert len(reports) == 3 # get the call report @@ -279,7 +300,7 @@ def test_a(): assert len(reports) == 2 report = reports[1] - def check_longrepr(longrepr): + def check_longrepr(longrepr: ExceptionChainRepr) -> None: """Check the attributes of the given longrepr object according to the test file. We can get away with testing both CollectReport and TestReport with this function because @@ -303,6 +324,7 @@ def check_longrepr(longrepr): assert report.failed assert len(report.sections) == 0 + assert isinstance(report.longrepr, ExceptionChainRepr) report.longrepr.addsection("title", "contents", "=") check_longrepr(report.longrepr) @@ -317,7 +339,7 @@ def check_longrepr(longrepr): # elsewhere and we do check the contents of the longrepr object after loading it. loaded_report.longrepr.toterminal(tw_mock) - def test_chained_exceptions_no_reprcrash(self, testdir, tw_mock) -> None: + def test_chained_exceptions_no_reprcrash(self, testdir: Testdir, tw_mock) -> None: """Regression test for tracebacks without a reprcrash (#5971) This happens notably on exceptions raised by multiprocess.pool: the exception transfer @@ -368,7 +390,7 @@ def test_a(): reports = reprec.getreports("pytest_runtest_logreport") - def check_longrepr(longrepr) -> None: + def check_longrepr(longrepr: object) -> None: assert isinstance(longrepr, ExceptionChainRepr) assert len(longrepr.chain) == 2 entry1, entry2 = longrepr.chain @@ -397,9 +419,12 @@ def check_longrepr(longrepr) -> None: # for same reasons as previous test, ensure we don't blow up here assert loaded_report.longrepr is not None + assert isinstance(loaded_report.longrepr, ExceptionChainRepr) loaded_report.longrepr.toterminal(tw_mock) - def test_report_prevent_ConftestImportFailure_hiding_exception(self, testdir): + def test_report_prevent_ConftestImportFailure_hiding_exception( + self, testdir: Testdir + ) -> None: sub_dir = testdir.tmpdir.join("ns").ensure_dir() sub_dir.join("conftest").new(ext=".py").write("import unknown") @@ -411,7 +436,7 @@ def test_report_prevent_ConftestImportFailure_hiding_exception(self, testdir): class TestHooks: """Test that the hooks are working correctly for plugins""" - def test_test_report(self, testdir, pytestconfig): + def test_test_report(self, testdir: Testdir, pytestconfig: Config) -> None: testdir.makepyfile( """ def test_a(): assert False @@ -433,7 +458,7 @@ def test_b(): pass assert new_rep.when == rep.when assert new_rep.outcome == rep.outcome - def test_collect_report(self, testdir, pytestconfig): + def test_collect_report(self, testdir: Testdir, pytestconfig: Config) -> None: testdir.makepyfile( """ def test_a(): assert False @@ -458,7 +483,9 @@ def test_b(): pass @pytest.mark.parametrize( "hook_name", ["pytest_runtest_logreport", "pytest_collectreport"] ) - def test_invalid_report_types(self, testdir, pytestconfig, hook_name): + def test_invalid_report_types( + self, testdir: Testdir, pytestconfig: Config, hook_name: str + ) -> None: testdir.makepyfile( """ def test_a(): pass From 44cd8a3a86354b2b686d0b64f2ac328aca574bc7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 4 Aug 2020 19:37:41 -0300 Subject: [PATCH 545/823] Demonstrate that plain unittest does not support async tests (#7607) Co-authored-by: Ran Benita --- .../unittest/test_unittest_plain_async.py | 6 ++++++ testing/test_unittest.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 testing/example_scripts/unittest/test_unittest_plain_async.py diff --git a/testing/example_scripts/unittest/test_unittest_plain_async.py b/testing/example_scripts/unittest/test_unittest_plain_async.py new file mode 100644 index 00000000000..78dfece684e --- /dev/null +++ b/testing/example_scripts/unittest/test_unittest_plain_async.py @@ -0,0 +1,6 @@ +import unittest + + +class Test(unittest.TestCase): + async def test_foo(self): + assert False diff --git a/testing/test_unittest.py b/testing/test_unittest.py index f9455e63b42..26fbf41cf79 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1241,3 +1241,18 @@ def test_asynctest_support(testdir): testdir.copy_example("unittest/test_unittest_asynctest.py") reprec = testdir.inline_run() reprec.assertoutcome(failed=1, passed=2) + + +def test_plain_unittest_does_not_support_async(testdir): + """Async functions in plain unittest.TestCase subclasses are not supported without plugins. + + This test exists here to avoid introducing this support by accident, leading users + to expect that it works, rather than doing so intentionally as a feature. + + See https://github.com/pytest-dev/pytest-asyncio/issues/180 for more context. + """ + testdir.copy_example("unittest/test_unittest_plain_async.py") + result = testdir.runpytest_subprocess() + result.stdout.fnmatch_lines( + ["*RuntimeWarning: coroutine * was never awaited", "*1 passed*"] + ) From a64298ff5e4d965b227e412c27677337f08a6ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Busche?= Date: Wed, 5 Aug 2020 17:03:27 +0200 Subject: [PATCH 546/823] Document registering markers in pyproject.toml (#7622) Co-authored-by: Bruno Oliveira --- doc/en/mark.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 6fb665fdfd2..1cdd1b8e6c4 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -43,7 +43,17 @@ You can register custom marks in your ``pytest.ini`` file like this: slow: marks tests as slow (deselect with '-m "not slow"') serial -Note that everything after the ``:`` is an optional description. +or in your ``pyproject.toml`` file like this: + +.. code-block:: toml + + [tool.pytest.ini_options] + markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "serial", + ] + +Note that everything past the ``:`` after the mark name is an optional description. Alternatively, you can register new markers programmatically in a :ref:`pytest_configure ` hook: From 67cb7ef673733628c2076a448806a78e64b274e4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 5 Aug 2020 15:24:08 -0300 Subject: [PATCH 547/823] Fix test_plain_unittest_does_not_support_async on pypy3 Fix #7624 --- testing/test_unittest.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 26fbf41cf79..c7b6bfcec92 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1,4 +1,5 @@ import gc +import sys from typing import List import pytest @@ -1253,6 +1254,14 @@ def test_plain_unittest_does_not_support_async(testdir): """ testdir.copy_example("unittest/test_unittest_plain_async.py") result = testdir.runpytest_subprocess() - result.stdout.fnmatch_lines( - ["*RuntimeWarning: coroutine * was never awaited", "*1 passed*"] - ) + if hasattr(sys, "pypy_version_info"): + # in PyPy we can't reliable get the warning about the coroutine not being awaited, + # because it depends on the coroutine being garbage collected; given that + # we are running in a subprocess, that's difficult to enforce + expected_lines = ["*1 passed*"] + else: + expected_lines = [ + "*RuntimeWarning: coroutine * was never awaited", + "*1 passed*", + ] + result.stdout.fnmatch_lines(expected_lines) From e0d0951945dd4d7a80b1324e4fd6ad59c3c334ad Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 4 Aug 2020 10:12:27 +0300 Subject: [PATCH 548/823] pathlib: add analogues to py.path.local's bestrelpath and common An equivalent for these py.path.local functions is needed for some upcoming py.path -> pathlib conversions. --- src/_pytest/pathlib.py | 32 ++++++++++++++++++++++++++++++++ testing/test_pathlib.py | 20 ++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index b3129020dee..c3235f8b84c 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -569,3 +569,35 @@ def visit( for entry in entries: if entry.is_dir(follow_symlinks=False) and recurse(entry): yield from visit(entry.path, recurse) + + +def commonpath(path1: Path, path2: Path) -> Optional[Path]: + """Return the common part shared with the other path, or None if there is + no common part.""" + try: + return Path(os.path.commonpath((str(path1), str(path2)))) + except ValueError: + return None + + +def bestrelpath(directory: Path, dest: Path) -> str: + """Return a string which is a relative path from directory to dest such + that directory/bestrelpath == dest. + + If no such path can be determined, returns dest. + """ + if dest == directory: + return os.curdir + # Find the longest common directory. + base = commonpath(directory, dest) + # Can be the case on Windows. + if not base: + return str(dest) + reldirectory = directory.relative_to(base) + reldest = dest.relative_to(base) + return os.path.join( + # Back from directory to base. + *([os.pardir] * len(reldirectory.parts)), + # Forward from base to dest. + *reldest.parts, + ) diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 74dac21d996..41228d6b095 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -6,6 +6,8 @@ import py import pytest +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import commonpath from _pytest.pathlib import ensure_deletable from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import get_extended_length_path_str @@ -381,3 +383,21 @@ def test_suppress_error_removing_lock(tmp_path): # check now that we can remove the lock file in normal circumstances assert ensure_deletable(path, consider_lock_dead_if_created_before=mtime + 30) assert not lock.is_file() + + +def test_bestrelpath() -> None: + curdir = Path("/foo/bar/baz/path") + assert bestrelpath(curdir, curdir) == "." + assert bestrelpath(curdir, curdir / "hello" / "world") == "hello" + os.sep + "world" + assert bestrelpath(curdir, curdir.parent / "sister") == ".." + os.sep + "sister" + assert bestrelpath(curdir, curdir.parent) == ".." + assert bestrelpath(curdir, Path("hello")) == "hello" + + +def test_commonpath() -> None: + path = Path("/foo/bar/baz/path") + subpath = path / "sampledir" + assert commonpath(path, subpath) == path + assert commonpath(subpath, path) == path + assert commonpath(Path(str(path) + "suffix"), path) == path.parent + assert commonpath(path, path.parent.parent) == path.parent.parent From 9e55288ba498a3a7ba9a909b40e0b70a0c69f9b3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 5 Aug 2020 18:36:41 +0300 Subject: [PATCH 549/823] pathlib: add absolutepath() as alternative to Path.resolve() Didn't call it absolute or absolute_path to avoid conflicts with possible variable names. Didn't call it abspath to avoid confusion with os.path.abspath. --- src/_pytest/pathlib.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index c3235f8b84c..4a249c8fd27 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -571,6 +571,15 @@ def visit( yield from visit(entry.path, recurse) +def absolutepath(path: Union[Path, str]) -> Path: + """Convert a path to an absolute path using os.path.abspath. + + Prefer this over Path.resolve() (see #6523). + Prefer this over Path.absolute() (not public, doesn't normalize). + """ + return Path(os.path.abspath(str(path))) + + def commonpath(path1: Path, path2: Path) -> Optional[Path]: """Return the common part shared with the other path, or None if there is no common part.""" From 70f3ad1c1f31b35d4004f92734b4afd6c8fbdecf Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 3 Aug 2020 17:46:35 +0300 Subject: [PATCH 550/823] config/findpaths: convert from py.path.local to pathlib --- src/_pytest/config/__init__.py | 5 +- src/_pytest/config/findpaths.py | 98 ++++++++++------ testing/test_config.py | 198 ++++++++++++++++++-------------- testing/test_findpaths.py | 102 ++++++++-------- 4 files changed, 227 insertions(+), 176 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 455a14b4040..6305cdbd57d 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1006,12 +1006,15 @@ def _initini(self, args: Sequence[str]) -> None: ns, unknown_args = self._parser.parse_known_and_unknown_args( args, namespace=copy.copy(self.option) ) - self.rootdir, self.inifile, self.inicfg = determine_setup( + rootpath, inipath, inicfg = determine_setup( ns.inifilename, ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, config=self, ) + self.rootdir = py.path.local(str(rootpath)) + self.inifile = py.path.local(str(inipath)) if inipath else None + self.inicfg = inicfg self._parser.extra_info["rootdir"] = self.rootdir self._parser.extra_info["inifile"] = self.inifile self._parser.addini("addopts", "extra command line options", "args") diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index be25fc82920..65120e48418 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -1,23 +1,28 @@ +import itertools import os +import sys from typing import Dict from typing import Iterable from typing import List from typing import Optional +from typing import Sequence from typing import Tuple from typing import Union import iniconfig -import py from .exceptions import UsageError from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import fail +from _pytest.pathlib import absolutepath +from _pytest.pathlib import commonpath +from _pytest.pathlib import Path if TYPE_CHECKING: from . import Config -def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig: +def _parse_ini_config(path: Path) -> iniconfig.IniConfig: """Parse the given generic '.ini' file using legacy IniConfig parser, returning the parsed object. @@ -30,7 +35,7 @@ def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig: def load_config_dict_from_file( - filepath: py.path.local, + filepath: Path, ) -> Optional[Dict[str, Union[str, List[str]]]]: """Load pytest configuration from the given file path, if supported. @@ -38,18 +43,18 @@ def load_config_dict_from_file( """ # Configuration from ini files are obtained from the [pytest] section, if present. - if filepath.ext == ".ini": + if filepath.suffix == ".ini": iniconfig = _parse_ini_config(filepath) if "pytest" in iniconfig: return dict(iniconfig["pytest"].items()) else: # "pytest.ini" files are always the source of configuration, even if empty. - if filepath.basename == "pytest.ini": + if filepath.name == "pytest.ini": return {} # '.cfg' files are considered if they contain a "[tool:pytest]" section. - elif filepath.ext == ".cfg": + elif filepath.suffix == ".cfg": iniconfig = _parse_ini_config(filepath) if "tool:pytest" in iniconfig.sections: @@ -60,7 +65,7 @@ def load_config_dict_from_file( fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) # '.toml' files are considered if they contain a [tool.pytest.ini_options] table. - elif filepath.ext == ".toml": + elif filepath.suffix == ".toml": import toml config = toml.load(str(filepath)) @@ -79,9 +84,9 @@ def make_scalar(v: object) -> Union[str, List[str]]: def locate_config( - args: Iterable[Union[str, py.path.local]] + args: Iterable[Path], ) -> Tuple[ - Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]], + Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]], ]: """Search in the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict).""" @@ -93,62 +98,77 @@ def locate_config( ] args = [x for x in args if not str(x).startswith("-")] if not args: - args = [py.path.local()] + args = [Path.cwd()] for arg in args: - arg = py.path.local(arg) - for base in arg.parts(reverse=True): + argpath = absolutepath(arg) + for base in itertools.chain((argpath,), reversed(argpath.parents)): for config_name in config_names: - p = base.join(config_name) - if p.isfile(): + p = base / config_name + if p.is_file(): ini_config = load_config_dict_from_file(p) if ini_config is not None: return base, p, ini_config return None, None, {} -def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local: - common_ancestor = None # type: Optional[py.path.local] +def get_common_ancestor(paths: Iterable[Path]) -> Path: + common_ancestor = None # type: Optional[Path] for path in paths: if not path.exists(): continue if common_ancestor is None: common_ancestor = path else: - if path.relto(common_ancestor) or path == common_ancestor: + if common_ancestor in path.parents or path == common_ancestor: continue - elif common_ancestor.relto(path): + elif path in common_ancestor.parents: common_ancestor = path else: - shared = path.common(common_ancestor) + shared = commonpath(path, common_ancestor) if shared is not None: common_ancestor = shared if common_ancestor is None: - common_ancestor = py.path.local() - elif common_ancestor.isfile(): - common_ancestor = common_ancestor.dirpath() + common_ancestor = Path.cwd() + elif common_ancestor.is_file(): + common_ancestor = common_ancestor.parent return common_ancestor -def get_dirs_from_args(args: Iterable[str]) -> List[py.path.local]: +def get_dirs_from_args(args: Iterable[str]) -> List[Path]: def is_option(x: str) -> bool: return x.startswith("-") def get_file_part_from_node_id(x: str) -> str: return x.split("::")[0] - def get_dir_from_path(path: py.path.local) -> py.path.local: - if path.isdir(): + def get_dir_from_path(path: Path) -> Path: + if path.is_dir(): return path - return py.path.local(path.dirname) + return path.parent + + if sys.version_info < (3, 8): + + def safe_exists(path: Path) -> bool: + # On Python<3.8, this can throw on paths that contain characters + # unrepresentable at the OS level. + try: + return path.exists() + except OSError: + return False + + else: + + def safe_exists(path: Path) -> bool: + return path.exists() # These look like paths but may not exist possible_paths = ( - py.path.local(get_file_part_from_node_id(arg)) + absolutepath(get_file_part_from_node_id(arg)) for arg in args if not is_option(arg) ) - return [get_dir_from_path(path) for path in possible_paths if path.exists()] + return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)] CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." @@ -156,15 +176,15 @@ def get_dir_from_path(path: py.path.local) -> py.path.local: def determine_setup( inifile: Optional[str], - args: List[str], + args: Sequence[str], rootdir_cmd_arg: Optional[str] = None, config: Optional["Config"] = None, -) -> Tuple[py.path.local, Optional[py.path.local], Dict[str, Union[str, List[str]]]]: +) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]: rootdir = None dirs = get_dirs_from_args(args) if inifile: - inipath_ = py.path.local(inifile) - inipath = inipath_ # type: Optional[py.path.local] + inipath_ = absolutepath(inifile) + inipath = inipath_ # type: Optional[Path] inicfg = load_config_dict_from_file(inipath_) or {} if rootdir_cmd_arg is None: rootdir = get_common_ancestor(dirs) @@ -172,8 +192,10 @@ def determine_setup( ancestor = get_common_ancestor(dirs) rootdir, inipath, inicfg = locate_config([ancestor]) if rootdir is None and rootdir_cmd_arg is None: - for possible_rootdir in ancestor.parts(reverse=True): - if possible_rootdir.join("setup.py").exists(): + for possible_rootdir in itertools.chain( + (ancestor,), reversed(ancestor.parents) + ): + if (possible_rootdir / "setup.py").is_file(): rootdir = possible_rootdir break else: @@ -181,16 +203,16 @@ def determine_setup( rootdir, inipath, inicfg = locate_config(dirs) if rootdir is None: if config is not None: - cwd = config.invocation_dir + cwd = config.invocation_params.dir else: - cwd = py.path.local() + cwd = Path.cwd() rootdir = get_common_ancestor([cwd, ancestor]) is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" if is_fs_root: rootdir = ancestor if rootdir_cmd_arg: - rootdir = py.path.local(os.path.expandvars(rootdir_cmd_arg)) - if not rootdir.isdir(): + rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) + if not rootdir.is_dir(): raise UsageError( "Directory '{}' not found. Check your '--rootdir' option.".format( rootdir diff --git a/testing/test_config.py b/testing/test_config.py index 26d2a3ef09b..346edb3304c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -21,17 +21,27 @@ from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import locate_config +from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import Path +from _pytest.pytester import Testdir class TestParseIni: @pytest.mark.parametrize( "section, filename", [("pytest", "pytest.ini"), ("tool:pytest", "setup.cfg")] ) - def test_getcfg_and_config(self, testdir, tmpdir, section, filename): - sub = tmpdir.mkdir("sub") - sub.chdir() - tmpdir.join(filename).write( + def test_getcfg_and_config( + self, + testdir: Testdir, + tmp_path: Path, + section: str, + filename: str, + monkeypatch: MonkeyPatch, + ) -> None: + sub = tmp_path / "sub" + sub.mkdir() + monkeypatch.chdir(sub) + (tmp_path / filename).write_text( textwrap.dedent( """\ [{section}] @@ -39,17 +49,14 @@ def test_getcfg_and_config(self, testdir, tmpdir, section, filename): """.format( section=section ) - ) + ), + encoding="utf-8", ) _, _, cfg = locate_config([sub]) assert cfg["name"] == "value" - config = testdir.parseconfigure(sub) + config = testdir.parseconfigure(str(sub)) assert config.inicfg["name"] == "value" - def test_getcfg_empty_path(self): - """Correctly handle zero length arguments (a la pytest '').""" - locate_config([""]) - def test_setupcfg_uses_toolpytest_with_pytest(self, testdir): p1 = testdir.makepyfile("def test(): pass") testdir.makefile( @@ -1168,16 +1175,17 @@ class pytest_something: class TestRootdir: - def test_simple_noini(self, tmpdir): - assert get_common_ancestor([tmpdir]) == tmpdir - a = tmpdir.mkdir("a") - assert get_common_ancestor([a, tmpdir]) == tmpdir - assert get_common_ancestor([tmpdir, a]) == tmpdir - with tmpdir.as_cwd(): - assert get_common_ancestor([]) == tmpdir - no_path = tmpdir.join("does-not-exist") - assert get_common_ancestor([no_path]) == tmpdir - assert get_common_ancestor([no_path.join("a")]) == tmpdir + def test_simple_noini(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + assert get_common_ancestor([tmp_path]) == tmp_path + a = tmp_path / "a" + a.mkdir() + assert get_common_ancestor([a, tmp_path]) == tmp_path + assert get_common_ancestor([tmp_path, a]) == tmp_path + monkeypatch.chdir(tmp_path) + assert get_common_ancestor([]) == tmp_path + no_path = tmp_path / "does-not-exist" + assert get_common_ancestor([no_path]) == tmp_path + assert get_common_ancestor([no_path / "a"]) == tmp_path @pytest.mark.parametrize( "name, contents", @@ -1190,44 +1198,49 @@ def test_simple_noini(self, tmpdir): pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"), ], ) - def test_with_ini(self, tmpdir: py.path.local, name: str, contents: str) -> None: - inifile = tmpdir.join(name) - inifile.write(contents) - - a = tmpdir.mkdir("a") - b = a.mkdir("b") - for args in ([str(tmpdir)], [str(a)], [str(b)]): - rootdir, parsed_inifile, _ = determine_setup(None, args) - assert rootdir == tmpdir - assert parsed_inifile == inifile - rootdir, parsed_inifile, ini_config = determine_setup(None, [str(b), str(a)]) - assert rootdir == tmpdir - assert parsed_inifile == inifile + def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None: + inipath = tmp_path / name + inipath.write_text(contents, "utf-8") + + a = tmp_path / "a" + a.mkdir() + b = a / "b" + b.mkdir() + for args in ([str(tmp_path)], [str(a)], [str(b)]): + rootpath, parsed_inipath, _ = determine_setup(None, args) + assert rootpath == tmp_path + assert parsed_inipath == inipath + rootpath, parsed_inipath, ini_config = determine_setup(None, [str(b), str(a)]) + assert rootpath == tmp_path + assert parsed_inipath == inipath assert ini_config == {"x": "10"} - @pytest.mark.parametrize("name", "setup.cfg tox.ini".split()) - def test_pytestini_overrides_empty_other(self, tmpdir: py.path.local, name) -> None: - inifile = tmpdir.ensure("pytest.ini") - a = tmpdir.mkdir("a") - a.ensure(name) - rootdir, parsed_inifile, _ = determine_setup(None, [str(a)]) - assert rootdir == tmpdir - assert parsed_inifile == inifile - - def test_setuppy_fallback(self, tmpdir: py.path.local) -> None: - a = tmpdir.mkdir("a") - a.ensure("setup.cfg") - tmpdir.ensure("setup.py") - rootdir, inifile, inicfg = determine_setup(None, [str(a)]) - assert rootdir == tmpdir - assert inifile is None + @pytest.mark.parametrize("name", ["setup.cfg", "tox.ini"]) + def test_pytestini_overrides_empty_other(self, tmp_path: Path, name: str) -> None: + inipath = tmp_path / "pytest.ini" + inipath.touch() + a = tmp_path / "a" + a.mkdir() + (a / name).touch() + rootpath, parsed_inipath, _ = determine_setup(None, [str(a)]) + assert rootpath == tmp_path + assert parsed_inipath == inipath + + def test_setuppy_fallback(self, tmp_path: Path) -> None: + a = tmp_path / "a" + a.mkdir() + (a / "setup.cfg").touch() + (tmp_path / "setup.py").touch() + rootpath, inipath, inicfg = determine_setup(None, [str(a)]) + assert rootpath == tmp_path + assert inipath is None assert inicfg == {} - def test_nothing(self, tmpdir: py.path.local, monkeypatch) -> None: - monkeypatch.chdir(str(tmpdir)) - rootdir, inifile, inicfg = determine_setup(None, [str(tmpdir)]) - assert rootdir == tmpdir - assert inifile is None + def test_nothing(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + monkeypatch.chdir(tmp_path) + rootpath, inipath, inicfg = determine_setup(None, [str(tmp_path)]) + assert rootpath == tmp_path + assert inipath is None assert inicfg == {} @pytest.mark.parametrize( @@ -1242,45 +1255,58 @@ def test_nothing(self, tmpdir: py.path.local, monkeypatch) -> None: ], ) def test_with_specific_inifile( - self, tmpdir: py.path.local, name: str, contents: str + self, tmp_path: Path, name: str, contents: str ) -> None: - p = tmpdir.ensure(name) - p.write(contents) - rootdir, inifile, ini_config = determine_setup(str(p), [str(tmpdir)]) - assert rootdir == tmpdir - assert inifile == p + p = tmp_path / name + p.touch() + p.write_text(contents, "utf-8") + rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)]) + assert rootpath == tmp_path + assert inipath == p assert ini_config == {"x": "10"} - def test_with_arg_outside_cwd_without_inifile(self, tmpdir, monkeypatch) -> None: - monkeypatch.chdir(str(tmpdir)) - a = tmpdir.mkdir("a") - b = tmpdir.mkdir("b") - rootdir, inifile, _ = determine_setup(None, [str(a), str(b)]) - assert rootdir == tmpdir + def test_with_arg_outside_cwd_without_inifile( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: + monkeypatch.chdir(tmp_path) + a = tmp_path / "a" + a.mkdir() + b = tmp_path / "b" + b.mkdir() + rootpath, inifile, _ = determine_setup(None, [str(a), str(b)]) + assert rootpath == tmp_path assert inifile is None - def test_with_arg_outside_cwd_with_inifile(self, tmpdir) -> None: - a = tmpdir.mkdir("a") - b = tmpdir.mkdir("b") - inifile = a.ensure("pytest.ini") - rootdir, parsed_inifile, _ = determine_setup(None, [str(a), str(b)]) - assert rootdir == a - assert inifile == parsed_inifile + def test_with_arg_outside_cwd_with_inifile(self, tmp_path: Path) -> None: + a = tmp_path / "a" + a.mkdir() + b = tmp_path / "b" + b.mkdir() + inipath = a / "pytest.ini" + inipath.touch() + rootpath, parsed_inipath, _ = determine_setup(None, [str(a), str(b)]) + assert rootpath == a + assert inipath == parsed_inipath @pytest.mark.parametrize("dirs", ([], ["does-not-exist"], ["a/does-not-exist"])) - def test_with_non_dir_arg(self, dirs, tmpdir) -> None: - with tmpdir.ensure(dir=True).as_cwd(): - rootdir, inifile, _ = determine_setup(None, dirs) - assert rootdir == tmpdir - assert inifile is None - - def test_with_existing_file_in_subdir(self, tmpdir) -> None: - a = tmpdir.mkdir("a") - a.ensure("exist") - with tmpdir.as_cwd(): - rootdir, inifile, _ = determine_setup(None, ["a/exist"]) - assert rootdir == tmpdir - assert inifile is None + def test_with_non_dir_arg( + self, dirs: Sequence[str], tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: + monkeypatch.chdir(tmp_path) + rootpath, inipath, _ = determine_setup(None, dirs) + assert rootpath == tmp_path + assert inipath is None + + def test_with_existing_file_in_subdir( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: + a = tmp_path / "a" + a.mkdir() + (a / "exists").touch() + monkeypatch.chdir(tmp_path) + rootpath, inipath, _ = determine_setup(None, ["a/exist"]) + assert rootpath == tmp_path + assert inipath is None class TestOverrideIniArgs: diff --git a/testing/test_findpaths.py b/testing/test_findpaths.py index 3de2ea21828..acb982b4cf4 100644 --- a/testing/test_findpaths.py +++ b/testing/test_findpaths.py @@ -1,74 +1,74 @@ from textwrap import dedent -import py - import pytest from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import load_config_dict_from_file +from _pytest.pathlib import Path class TestLoadConfigDictFromFile: - def test_empty_pytest_ini(self, tmpdir): + def test_empty_pytest_ini(self, tmp_path: Path) -> None: """pytest.ini files are always considered for configuration, even if empty""" - fn = tmpdir.join("pytest.ini") - fn.write("") + fn = tmp_path / "pytest.ini" + fn.write_text("", encoding="utf-8") assert load_config_dict_from_file(fn) == {} - def test_pytest_ini(self, tmpdir): + def test_pytest_ini(self, tmp_path: Path) -> None: """[pytest] section in pytest.ini files is read correctly""" - fn = tmpdir.join("pytest.ini") - fn.write("[pytest]\nx=1") + fn = tmp_path / "pytest.ini" + fn.write_text("[pytest]\nx=1", encoding="utf-8") assert load_config_dict_from_file(fn) == {"x": "1"} - def test_custom_ini(self, tmpdir): + def test_custom_ini(self, tmp_path: Path) -> None: """[pytest] section in any .ini file is read correctly""" - fn = tmpdir.join("custom.ini") - fn.write("[pytest]\nx=1") + fn = tmp_path / "custom.ini" + fn.write_text("[pytest]\nx=1", encoding="utf-8") assert load_config_dict_from_file(fn) == {"x": "1"} - def test_custom_ini_without_section(self, tmpdir): + def test_custom_ini_without_section(self, tmp_path: Path) -> None: """Custom .ini files without [pytest] section are not considered for configuration""" - fn = tmpdir.join("custom.ini") - fn.write("[custom]") + fn = tmp_path / "custom.ini" + fn.write_text("[custom]", encoding="utf-8") assert load_config_dict_from_file(fn) is None - def test_custom_cfg_file(self, tmpdir): + def test_custom_cfg_file(self, tmp_path: Path) -> None: """Custom .cfg files without [tool:pytest] section are not considered for configuration""" - fn = tmpdir.join("custom.cfg") - fn.write("[custom]") + fn = tmp_path / "custom.cfg" + fn.write_text("[custom]", encoding="utf-8") assert load_config_dict_from_file(fn) is None - def test_valid_cfg_file(self, tmpdir): + def test_valid_cfg_file(self, tmp_path: Path) -> None: """Custom .cfg files with [tool:pytest] section are read correctly""" - fn = tmpdir.join("custom.cfg") - fn.write("[tool:pytest]\nx=1") + fn = tmp_path / "custom.cfg" + fn.write_text("[tool:pytest]\nx=1", encoding="utf-8") assert load_config_dict_from_file(fn) == {"x": "1"} - def test_unsupported_pytest_section_in_cfg_file(self, tmpdir): + def test_unsupported_pytest_section_in_cfg_file(self, tmp_path: Path) -> None: """.cfg files with [pytest] section are no longer supported and should fail to alert users""" - fn = tmpdir.join("custom.cfg") - fn.write("[pytest]") + fn = tmp_path / "custom.cfg" + fn.write_text("[pytest]", encoding="utf-8") with pytest.raises(pytest.fail.Exception): load_config_dict_from_file(fn) - def test_invalid_toml_file(self, tmpdir): + def test_invalid_toml_file(self, tmp_path: Path) -> None: """.toml files without [tool.pytest.ini_options] are not considered for configuration.""" - fn = tmpdir.join("myconfig.toml") - fn.write( + fn = tmp_path / "myconfig.toml" + fn.write_text( dedent( """ [build_system] x = 1 """ - ) + ), + encoding="utf-8", ) assert load_config_dict_from_file(fn) is None - def test_valid_toml_file(self, tmpdir): + def test_valid_toml_file(self, tmp_path: Path) -> None: """.toml files with [tool.pytest.ini_options] are read correctly, including changing data types to str/list for compatibility with other configuration options.""" - fn = tmpdir.join("myconfig.toml") - fn.write( + fn = tmp_path / "myconfig.toml" + fn.write_text( dedent( """ [tool.pytest.ini_options] @@ -77,7 +77,8 @@ def test_valid_toml_file(self, tmpdir): values = ["tests", "integration"] name = "foo" """ - ) + ), + encoding="utf-8", ) assert load_config_dict_from_file(fn) == { "x": "1", @@ -88,23 +89,22 @@ def test_valid_toml_file(self, tmpdir): class TestCommonAncestor: - def test_has_ancestor(self, tmpdir): - fn1 = tmpdir.join("foo/bar/test_1.py").ensure(file=1) - fn2 = tmpdir.join("foo/zaz/test_2.py").ensure(file=1) - assert get_common_ancestor([fn1, fn2]) == tmpdir.join("foo") - assert get_common_ancestor([py.path.local(fn1.dirname), fn2]) == tmpdir.join( - "foo" - ) - assert get_common_ancestor( - [py.path.local(fn1.dirname), py.path.local(fn2.dirname)] - ) == tmpdir.join("foo") - assert get_common_ancestor([fn1, py.path.local(fn2.dirname)]) == tmpdir.join( - "foo" - ) - - def test_single_dir(self, tmpdir): - assert get_common_ancestor([tmpdir]) == tmpdir - - def test_single_file(self, tmpdir): - fn = tmpdir.join("foo.py").ensure(file=1) - assert get_common_ancestor([fn]) == tmpdir + def test_has_ancestor(self, tmp_path: Path) -> None: + fn1 = tmp_path / "foo" / "bar" / "test_1.py" + fn1.parent.mkdir(parents=True) + fn1.touch() + fn2 = tmp_path / "foo" / "zaz" / "test_2.py" + fn2.parent.mkdir(parents=True) + fn2.touch() + assert get_common_ancestor([fn1, fn2]) == tmp_path / "foo" + assert get_common_ancestor([fn1.parent, fn2]) == tmp_path / "foo" + assert get_common_ancestor([fn1.parent, fn2.parent]) == tmp_path / "foo" + assert get_common_ancestor([fn1, fn2.parent]) == tmp_path / "foo" + + def test_single_dir(self, tmp_path: Path) -> None: + assert get_common_ancestor([tmp_path]) == tmp_path + + def test_single_file(self, tmp_path: Path) -> None: + fn = tmp_path / "foo.py" + fn.touch() + assert get_common_ancestor([fn]) == tmp_path From f8c4e038fde9e58732ef6ffad77cc25d0746cbc3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 2 Aug 2020 17:56:36 +0300 Subject: [PATCH 551/823] Replace some usages of py.path.local --- extra/get_issues.py | 8 ++++---- src/_pytest/_code/code.py | 27 ++++++++++++++++++--------- src/_pytest/compat.py | 14 +++++++++----- src/_pytest/config/__init__.py | 4 ++-- src/_pytest/fixtures.py | 5 +++-- src/_pytest/python.py | 6 +++--- src/_pytest/resultlog.py | 4 +--- testing/acceptance_test.py | 2 +- 8 files changed, 41 insertions(+), 29 deletions(-) diff --git a/extra/get_issues.py b/extra/get_issues.py index c264b26446d..4aaa3c3ec31 100644 --- a/extra/get_issues.py +++ b/extra/get_issues.py @@ -1,6 +1,6 @@ import json +from pathlib import Path -import py import requests issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues" @@ -31,12 +31,12 @@ def get_issues(): def main(args): - cachefile = py.path.local(args.cache) + cachefile = Path(args.cache) if not cachefile.exists() or args.refresh: issues = get_issues() - cachefile.write(json.dumps(issues)) + cachefile.write_text(json.dumps(issues), "utf-8") else: - issues = json.loads(cachefile.read()) + issues = json.loads(cachefile.read_text("utf-8")) open_issues = [x for x in issues if x["state"] == "open"] diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index b2e4fcd33d9..420135b4e02 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -41,6 +41,7 @@ from _pytest.compat import get_real_func from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING +from _pytest.pathlib import Path if TYPE_CHECKING: from typing import Type @@ -1190,12 +1191,12 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: # note: if we need to add more paths than what we have now we should probably use a list # for better maintenance. -_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc")) +_PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc")) # pluggy is either a package or a single module depending on the version -if _PLUGGY_DIR.basename == "__init__.py": - _PLUGGY_DIR = _PLUGGY_DIR.dirpath() -_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath() -_PY_DIR = py.path.local(py.__file__).dirpath() +if _PLUGGY_DIR.name == "__init__.py": + _PLUGGY_DIR = _PLUGGY_DIR.parent +_PYTEST_DIR = Path(_pytest.__file__).parent +_PY_DIR = Path(py.__file__).parent def filter_traceback(entry: TracebackEntry) -> bool: @@ -1213,9 +1214,17 @@ def filter_traceback(entry: TracebackEntry) -> bool: is_generated = "<" in raw_filename and ">" in raw_filename if is_generated: return False + # entry.path might point to a non-existing file, in which case it will # also return a str object. See #1133. - p = py.path.local(entry.path) - return ( - not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR) - ) + p = Path(entry.path) + + parents = p.parents + if _PLUGGY_DIR in parents: + return False + if _PYTEST_DIR in parents: + return False + if _PY_DIR in parents: + return False + + return True diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index ff98492dcff..4b46d9c950b 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -18,7 +18,6 @@ from typing import Union import attr -import py from _pytest._io.saferepr import saferepr from _pytest.outcomes import fail @@ -104,13 +103,18 @@ def is_async_function(func: object) -> bool: ) -def getlocation(function, curdir=None) -> str: +def getlocation(function, curdir: Optional[str] = None) -> str: + from _pytest.pathlib import Path + function = get_real_func(function) - fn = py.path.local(inspect.getfile(function)) + fn = Path(inspect.getfile(function)) lineno = function.__code__.co_firstlineno if curdir is not None: - relfn = fn.relto(curdir) - if relfn: + try: + relfn = fn.relative_to(curdir) + except ValueError: + pass + else: return "%s:%d" % (relfn, lineno + 1) return "%s:%d" % (fn, lineno + 1) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6305cdbd57d..453dd834537 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -123,7 +123,7 @@ def filter_traceback_for_conftest_import_failure( def main( - args: Optional[List[str]] = None, + args: Optional[Union[List[str], py.path.local]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, ) -> Union[int, ExitCode]: """Perform an in-process test run. @@ -1308,7 +1308,7 @@ def _getconftest_pathlist( values = [] # type: List[py.path.local] for relroot in relroots: if not isinstance(relroot, py.path.local): - relroot = relroot.replace("/", py.path.local.sep) + relroot = relroot.replace("/", os.sep) relroot = modpath.join(relroot, abs=True) values.append(relroot) return values diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d2ff6203b4b..846cc2bb132 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1,5 +1,6 @@ import functools import inspect +import os import sys import warnings from collections import defaultdict @@ -1515,8 +1516,8 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: # by their test id). if p.basename.startswith("conftest.py"): nodeid = p.dirpath().relto(self.config.rootdir) - if p.sep != nodes.SEP: - nodeid = nodeid.replace(p.sep, nodes.SEP) + if os.sep != nodes.SEP: + nodeid = nodeid.replace(os.sep, nodes.SEP) self.parsefactories(plugin, nodeid) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 7416245653d..0661f340209 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1339,7 +1339,7 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None: verbose = config.getvalue("verbose") def get_best_relpath(func): - loc = getlocation(func, curdir) + loc = getlocation(func, str(curdir)) return curdir.bestrelpath(py.path.local(loc)) def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: @@ -1404,7 +1404,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: if not fixturedefs: continue for fixturedef in fixturedefs: - loc = getlocation(fixturedef.func, curdir) + loc = getlocation(fixturedef.func, str(curdir)) if (fixturedef.argname, loc) in seen: continue seen.add((fixturedef.argname, loc)) @@ -1434,7 +1434,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: if verbose > 0: tw.write(" -- %s" % bestrel, yellow=True) tw.write("\n") - loc = getlocation(fixturedef.func, curdir) + loc = getlocation(fixturedef.func, str(curdir)) doc = inspect.getdoc(fixturedef.func) if doc: write_docstring(tw, doc) diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index c043c749f87..686f7f3b0af 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -3,8 +3,6 @@ from typing import IO from typing import Union -import py - from _pytest._code.code import ExceptionRepr from _pytest.config import Config from _pytest.config.argparsing import Parser @@ -106,5 +104,5 @@ def pytest_internalerror(self, excrepr: ExceptionRepr) -> None: if excrepr.reprcrash is not None: path = excrepr.reprcrash.path else: - path = "cwd:%s" % py.path.local() + path = "cwd:%s" % os.getcwd() self.write_log_entry(path, "!", str(excrepr)) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 3172dad7cc3..b37cfa0cbd9 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -586,7 +586,7 @@ def test_invoke_with_invalid_type(self) -> None: ): pytest.main("-h") # type: ignore[arg-type] - def test_invoke_with_path(self, tmpdir, capsys): + def test_invoke_with_path(self, tmpdir: py.path.local, capsys) -> None: retcode = pytest.main(tmpdir) assert retcode == ExitCode.NO_TESTS_COLLECTED out, err = capsys.readouterr() From a27c539a85b6c908af8f3a249d27e990ae0ded84 Mon Sep 17 00:00:00 2001 From: Sam Estep Date: Fri, 7 Aug 2020 15:22:10 -0700 Subject: [PATCH 552/823] Fix typos in Ali Afshar's name The correct name is visible in the Bitbucket link. --- doc/en/example/nonpython.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index d15b7ae8bdd..464a6c6cede 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -12,7 +12,7 @@ A basic example for specifying tests in Yaml files .. _`pytest-yamlwsgi`: http://bitbucket.org/aafshar/pytest-yamlwsgi/src/tip/pytest_yamlwsgi.py .. _`PyYAML`: https://pypi.org/project/PyYAML/ -Here is an example ``conftest.py`` (extracted from Ali Afshnars special purpose `pytest-yamlwsgi`_ plugin). This ``conftest.py`` will collect ``test*.yaml`` files and will execute the yaml-formatted content as custom tests: +Here is an example ``conftest.py`` (extracted from Ali Afshar's special purpose `pytest-yamlwsgi`_ plugin). This ``conftest.py`` will collect ``test*.yaml`` files and will execute the yaml-formatted content as custom tests: .. include:: nonpython/conftest.py :literal: From 8a66f0a96d51b35f3e948ad6c5e140fb05e96c52 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 9 Aug 2020 21:20:07 +0300 Subject: [PATCH 553/823] capture: overcome a mypy limitation by making CaptureResult a regular class See the code comment for the rationale. --- src/_pytest/capture.py | 56 +++++++++++++++++++++++++++++++++++++++-- testing/test_capture.py | 31 +++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 99a587fcb7d..bf17e0eb888 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -1,6 +1,6 @@ """Per-test stdout/stderr capturing mechanism.""" -import collections import contextlib +import functools import io import os import sys @@ -488,7 +488,59 @@ def writeorg(self, data): # MultiCapture -CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"]) + +# This class was a namedtuple, but due to mypy limitation[0] it could not be +# made generic, so was replaced by a regular class which tries to emulate the +# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can +# make it a namedtuple again. +# [0]: https://github.com/python/mypy/issues/685 +@functools.total_ordering +class CaptureResult: + """The result of :method:`CaptureFixture.readouterr`.""" + + # Can't use slots in Python<3.5.3 due to https://bugs.python.org/issue31272 + if sys.version_info >= (3, 5, 3): + __slots__ = ("out", "err") + + def __init__(self, out, err) -> None: + self.out = out + self.err = err + + def __len__(self) -> int: + return 2 + + def __iter__(self): + return iter((self.out, self.err)) + + def __getitem__(self, item: int): + return tuple(self)[item] + + def _replace(self, out=None, err=None) -> "CaptureResult": + return CaptureResult( + out=self.out if out is None else out, err=self.err if err is None else err + ) + + def count(self, value) -> int: + return tuple(self).count(value) + + def index(self, value) -> int: + return tuple(self).index(value) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, (CaptureResult, tuple)): + return NotImplemented + return tuple(self) == tuple(other) + + def __hash__(self) -> int: + return hash(tuple(self)) + + def __lt__(self, other: object) -> bool: + if not isinstance(other, (CaptureResult, tuple)): + return NotImplemented + return tuple(self) < tuple(other) + + def __repr__(self) -> str: + return "CaptureResult(out={!r}, err={!r})".format(self.out, self.err) class MultiCapture: diff --git a/testing/test_capture.py b/testing/test_capture.py index 15077a3e974..92a0a3e41ff 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -14,6 +14,7 @@ from _pytest import capture from _pytest.capture import _get_multicapture from _pytest.capture import CaptureManager +from _pytest.capture import CaptureResult from _pytest.capture import MultiCapture from _pytest.config import ExitCode @@ -856,6 +857,36 @@ def test_dontreadfrominput(): f.close() # just for completeness +def test_captureresult() -> None: + cr = CaptureResult("out", "err") + assert len(cr) == 2 + assert cr.out == "out" + assert cr.err == "err" + out, err = cr + assert out == "out" + assert err == "err" + assert cr[0] == "out" + assert cr[1] == "err" + assert cr == cr + assert cr == CaptureResult("out", "err") + assert cr != CaptureResult("wrong", "err") + assert cr == ("out", "err") + assert cr != ("out", "wrong") + assert hash(cr) == hash(CaptureResult("out", "err")) + assert hash(cr) == hash(("out", "err")) + assert hash(cr) != hash(("out", "wrong")) + assert cr < ("z",) + assert cr < ("z", "b") + assert cr < ("z", "b", "c") + assert cr.count("err") == 1 + assert cr.count("wrong") == 0 + assert cr.index("err") == 1 + with pytest.raises(ValueError): + assert cr.index("wrong") == 0 + assert next(iter(cr)) == "out" + assert cr._replace(err="replaced") == ("out", "replaced") + + @pytest.fixture def tmpfile(testdir) -> Generator[BinaryIO, None, None]: f = testdir.makepyfile("").open("wb+") From acc9310c17eac6764beb36e9970ca84883369a2f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 9 Aug 2020 20:15:19 +0300 Subject: [PATCH 554/823] capture: add type annotations to CaptureFixture It now has a str/bytes type parameter. --- src/_pytest/capture.py | 60 ++++++++++++++++++++++------------------- testing/test_capture.py | 12 ++++++--- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index bf17e0eb888..7daf36dddbb 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -6,7 +6,11 @@ import sys from io import UnsupportedOperation from tempfile import TemporaryFile +from typing import Any +from typing import AnyStr from typing import Generator +from typing import Generic +from typing import Iterator from typing import Optional from typing import TextIO from typing import Tuple @@ -495,32 +499,34 @@ def writeorg(self, data): # make it a namedtuple again. # [0]: https://github.com/python/mypy/issues/685 @functools.total_ordering -class CaptureResult: +class CaptureResult(Generic[AnyStr]): """The result of :method:`CaptureFixture.readouterr`.""" # Can't use slots in Python<3.5.3 due to https://bugs.python.org/issue31272 if sys.version_info >= (3, 5, 3): __slots__ = ("out", "err") - def __init__(self, out, err) -> None: - self.out = out - self.err = err + def __init__(self, out: AnyStr, err: AnyStr) -> None: + self.out = out # type: AnyStr + self.err = err # type: AnyStr def __len__(self) -> int: return 2 - def __iter__(self): + def __iter__(self) -> Iterator[AnyStr]: return iter((self.out, self.err)) - def __getitem__(self, item: int): + def __getitem__(self, item: int) -> AnyStr: return tuple(self)[item] - def _replace(self, out=None, err=None) -> "CaptureResult": + def _replace( + self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None + ) -> "CaptureResult[AnyStr]": return CaptureResult( out=self.out if out is None else out, err=self.err if err is None else err ) - def count(self, value) -> int: + def count(self, value: AnyStr) -> int: return tuple(self).count(value) def index(self, value) -> int: @@ -543,7 +549,7 @@ def __repr__(self) -> str: return "CaptureResult(out={!r}, err={!r})".format(self.out, self.err) -class MultiCapture: +class MultiCapture(Generic[AnyStr]): _state = None _in_suspended = False @@ -566,7 +572,7 @@ def start_capturing(self) -> None: if self.err: self.err.start() - def pop_outerr_to_orig(self): + def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]: """Pop current snapshot out/err capture and flush to orig streams.""" out, err = self.readouterr() if out: @@ -607,7 +613,7 @@ def stop_capturing(self) -> None: if self.in_: self.in_.done() - def readouterr(self) -> CaptureResult: + def readouterr(self) -> CaptureResult[AnyStr]: if self.out: out = self.out.snap() else: @@ -619,7 +625,7 @@ def readouterr(self) -> CaptureResult: return CaptureResult(out, err) -def _get_multicapture(method: "_CaptureMethod") -> MultiCapture: +def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: if method == "fd": return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) elif method == "sys": @@ -657,8 +663,8 @@ class CaptureManager: def __init__(self, method: "_CaptureMethod") -> None: self._method = method - self._global_capturing = None # type: Optional[MultiCapture] - self._capture_fixture = None # type: Optional[CaptureFixture] + self._global_capturing = None # type: Optional[MultiCapture[str]] + self._capture_fixture = None # type: Optional[CaptureFixture[Any]] def __repr__(self) -> str: return "".format( @@ -707,13 +713,13 @@ def resume(self) -> None: self.resume_global_capture() self.resume_fixture() - def read_global_capture(self): + def read_global_capture(self) -> CaptureResult[str]: assert self._global_capturing is not None return self._global_capturing.readouterr() # Fixture Control - def set_fixture(self, capture_fixture: "CaptureFixture") -> None: + def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None: if self._capture_fixture: current_fixture = self._capture_fixture.request.fixturename requested_fixture = capture_fixture.request.fixturename @@ -812,14 +818,14 @@ def pytest_internalerror(self) -> None: self.stop_global_capturing() -class CaptureFixture: +class CaptureFixture(Generic[AnyStr]): """Object returned by the :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary` fixtures.""" def __init__(self, captureclass, request: SubRequest) -> None: self.captureclass = captureclass self.request = request - self._capture = None # type: Optional[MultiCapture] + self._capture = None # type: Optional[MultiCapture[AnyStr]] self._captured_out = self.captureclass.EMPTY_BUFFER self._captured_err = self.captureclass.EMPTY_BUFFER @@ -838,7 +844,7 @@ def close(self) -> None: self._capture.stop_capturing() self._capture = None - def readouterr(self): + def readouterr(self) -> CaptureResult[AnyStr]: """Read and return the captured output so far, resetting the internal buffer. @@ -877,7 +883,7 @@ def disabled(self) -> Generator[None, None, None]: @pytest.fixture -def capsys(request: SubRequest) -> Generator[CaptureFixture, None, None]: +def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method @@ -885,7 +891,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture, None, None]: ``out`` and ``err`` will be ``text`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture(SysCapture, request) + capture_fixture = CaptureFixture[str](SysCapture, request) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture @@ -894,7 +900,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture, None, None]: @pytest.fixture -def capsysbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]: +def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` @@ -902,7 +908,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]: ``out`` and ``err`` will be ``bytes`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture(SysCaptureBinary, request) + capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture @@ -911,7 +917,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]: @pytest.fixture -def capfd(request: SubRequest) -> Generator[CaptureFixture, None, None]: +def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: """Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -919,7 +925,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture, None, None]: ``out`` and ``err`` will be ``text`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture(FDCapture, request) + capture_fixture = CaptureFixture[str](FDCapture, request) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture @@ -928,7 +934,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture, None, None]: @pytest.fixture -def capfdbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]: +def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: """Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -936,7 +942,7 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture, None, None]: ``out`` and ``err`` will be ``byte`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture(FDCaptureBinary, request) + capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture diff --git a/testing/test_capture.py b/testing/test_capture.py index 92a0a3e41ff..b7545d73f76 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -22,7 +22,9 @@ # pylib 1.4.20.dev2 (rev 13d9af95547e) -def StdCaptureFD(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture: +def StdCaptureFD( + out: bool = True, err: bool = True, in_: bool = True +) -> MultiCapture[str]: return capture.MultiCapture( in_=capture.FDCapture(0) if in_ else None, out=capture.FDCapture(1) if out else None, @@ -30,7 +32,9 @@ def StdCaptureFD(out: bool = True, err: bool = True, in_: bool = True) -> MultiC ) -def StdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture: +def StdCapture( + out: bool = True, err: bool = True, in_: bool = True +) -> MultiCapture[str]: return capture.MultiCapture( in_=capture.SysCapture(0) if in_ else None, out=capture.SysCapture(1) if out else None, @@ -38,7 +42,9 @@ def StdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCap ) -def TeeStdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture: +def TeeStdCapture( + out: bool = True, err: bool = True, in_: bool = True +) -> MultiCapture[str]: return capture.MultiCapture( in_=capture.SysCapture(0, tee=True) if in_ else None, out=capture.SysCapture(1, tee=True) if out else None, From 15d8293241e5cc3e6256f41963ea3204d5b55117 Mon Sep 17 00:00:00 2001 From: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Date: Wed, 12 Aug 2020 19:47:34 +0200 Subject: [PATCH 555/823] Remove faq.rst from docs (#7635) --- changelog/1477.doc.rst | 1 + doc/en/adopt.rst | 5 +- doc/en/announce/release-2.3.0.rst | 2 +- doc/en/changelog.rst | 2 +- doc/en/contents.rst | 1 - doc/en/faq.rst | 158 ------------------------------ 6 files changed, 6 insertions(+), 163 deletions(-) create mode 100644 changelog/1477.doc.rst delete mode 100644 doc/en/faq.rst diff --git a/changelog/1477.doc.rst b/changelog/1477.doc.rst new file mode 100644 index 00000000000..fbe12597f07 --- /dev/null +++ b/changelog/1477.doc.rst @@ -0,0 +1 @@ +Removed faq.rst and its reference in contents.rst. diff --git a/doc/en/adopt.rst b/doc/en/adopt.rst index e3c0477bc0e..82e2111ed3b 100644 --- a/doc/en/adopt.rst +++ b/doc/en/adopt.rst @@ -45,7 +45,7 @@ Partner projects, sign up here! (by 22 March) What does it mean to "adopt pytest"? ----------------------------------------- -There can be many different definitions of "success". Pytest can run many `nose and unittest`_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right? +There can be many different definitions of "success". Pytest can run many nose_ and unittest_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right? Progressive success might look like: @@ -63,7 +63,8 @@ Progressive success might look like: It may be after the month is up, the partner project decides that pytest is not right for it. That's okay - hopefully the pytest team will also learn something about its weaknesses or deficiencies. -.. _`nose and unittest`: faq.html#how-does-pytest-relate-to-nose-and-unittest +.. _nose: nose.html +.. _unittest: unittest.html .. _assert: assert.html .. _pycmd: https://bitbucket.org/hpk42/pycmd/overview .. _`setUp/tearDown methods`: xunit_setup.html diff --git a/doc/en/announce/release-2.3.0.rst b/doc/en/announce/release-2.3.0.rst index d938192bb0d..bdd92a98fde 100644 --- a/doc/en/announce/release-2.3.0.rst +++ b/doc/en/announce/release-2.3.0.rst @@ -94,7 +94,7 @@ Changes between 2.2.4 and 2.3.0 - pluginmanager.register(...) now raises ValueError if the plugin has been already registered or the name is taken -- fix issue159: improve http://pytest.org/en/stable/faq.html +- fix issue159: improve https://docs.pytest.org/en/6.0.1/faq.html especially with respect to the "magic" history, also mention pytest-django, trial and unittest integration. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 05ce4caea8a..ff7fb0c563d 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -7403,7 +7403,7 @@ Bug fixes: - pluginmanager.register(...) now raises ValueError if the plugin has been already registered or the name is taken -- fix issue159: improve http://pytest.org/en/stable/faq.html +- fix issue159: improve https://docs.pytest.org/en/6.0.1/faq.html especially with respect to the "magic" history, also mention pytest-django, trial and unittest integration. diff --git a/doc/en/contents.rst b/doc/en/contents.rst index c623d0602ab..58a08744ced 100644 --- a/doc/en/contents.rst +++ b/doc/en/contents.rst @@ -38,7 +38,6 @@ Full pytest documentation customize example/index bash-completion - faq backwards-compatibility deprecations diff --git a/doc/en/faq.rst b/doc/en/faq.rst deleted file mode 100644 index c281debe8cc..00000000000 --- a/doc/en/faq.rst +++ /dev/null @@ -1,158 +0,0 @@ -Some Issues and Questions -================================== - -.. note:: - - This FAQ is here only mostly for historic reasons. Checkout - `pytest Q&A at Stackoverflow `_ - for many questions and answers related to pytest and/or use - :ref:`contact channels` to get help. - -On naming, nosetests, licensing and magic ------------------------------------------------- - -How does pytest relate to nose and unittest? -+++++++++++++++++++++++++++++++++++++++++++++++++ - -``pytest`` and nose_ share basic philosophy when it comes -to running and writing Python tests. In fact, you can run many tests -written for nose with ``pytest``. nose_ was originally created -as a clone of ``pytest`` when ``pytest`` was in the ``0.8`` release -cycle. Note that starting with pytest-2.0 support for running unittest -test suites is majorly improved. - -how does pytest relate to twisted's trial? -++++++++++++++++++++++++++++++++++++++++++++++ - -Since some time ``pytest`` has builtin support for supporting tests -written using trial. It does not itself start a reactor, however, -and does not handle Deferreds returned from a test in pytest style. -If you are using trial's unittest.TestCase chances are that you can -just run your tests even if you return Deferreds. In addition, -there also is a dedicated `pytest-twisted -`_ plugin which allows you to -return deferreds from pytest-style tests, allowing the use of -:ref:`fixtures ` and other features. - -how does pytest work with Django? -++++++++++++++++++++++++++++++++++++++++++++++ - -In 2012, some work is going into the `pytest-django plugin `_. It substitutes the usage of Django's -``manage.py test`` and allows the use of all pytest features_ most of which -are not available from Django directly. - -.. _features: features.html - - -What's this "magic" with pytest? (historic notes) -++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -Around 2007 (version ``0.8``) some people thought that ``pytest`` -was using too much "magic". It had been part of the `pylib`_ which -contains a lot of unrelated python library code. Around 2010 there -was a major cleanup refactoring, which removed unused or deprecated code -and resulted in the new ``pytest`` PyPI package which strictly contains -only test-related code. This release also brought a complete pluginification -such that the core is around 300 lines of code and everything else is -implemented in plugins. Thus ``pytest`` today is a small, universally runnable -and customizable testing framework for Python. Note, however, that -``pytest`` uses metaprogramming techniques and reading its source is -thus likely not something for Python beginners. - -A second "magic" issue was the assert statement debugging feature. -Nowadays, ``pytest`` explicitly rewrites assert statements in test modules -in order to provide more useful :ref:`assert feedback `. -This completely avoids previous issues of confusing assertion-reporting. -It also means, that you can use Python's ``-O`` optimization without losing -assertions in test modules. - -You can also turn off all assertion interaction using the -``--assert=plain`` option. - -.. _`py namespaces`: index.html -.. _`py/__init__.py`: http://bitbucket.org/hpk42/py-trunk/src/trunk/py/__init__.py - - -Why can I use both ``pytest`` and ``py.test`` commands? -+++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -pytest used to be part of the py package, which provided several developer -utilities, all starting with ``py.``, thus providing nice TAB-completion. -If you install ``pip install pycmd`` you get these tools from a separate -package. Once ``pytest`` became a separate package, the ``py.test`` name was -retained due to avoid a naming conflict with another tool. This conflict was -eventually resolved, and the ``pytest`` command was therefore introduced. In -future versions of pytest, we may deprecate and later remove the ``py.test`` -command to avoid perpetuating the confusion. - -pytest fixtures, parametrized tests -------------------------------------------------------- - -.. _funcargs: funcargs.html - -Is using pytest fixtures versus xUnit setup a style question? -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -For simple applications and for people experienced with nose_ or -unittest-style test setup using `xUnit style setup`_ probably -feels natural. For larger test suites, parametrized testing -or setup of complex test resources using fixtures_ may feel more natural. -Moreover, fixtures are ideal for writing advanced test support -code (like e.g. the monkeypatch_, the tmpdir_ or capture_ fixtures) -because the support code can register setup/teardown functions -in a managed class/module/function scope. - -.. _monkeypatch: monkeypatch.html -.. _tmpdir: tmpdir.html -.. _capture: capture.html -.. _fixtures: fixture.html - -.. _`why pytest_pyfuncarg__ methods?`: - -.. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration - -Can I yield multiple values from a fixture function? -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -There are two conceptual reasons why yielding from a factory function -is not possible: - -* If multiple factories yielded values there would - be no natural place to determine the combination - policy - in real-world examples some combinations - often should not run. - -* Calling factories for obtaining test function arguments - is part of setting up and running a test. At that - point it is not possible to add new test calls to - the test collection anymore. - -However, with pytest-2.3 you can use the :ref:`@pytest.fixture` decorator -and specify ``params`` so that all tests depending on the factory-created -resource will run multiple times with different parameters. - -You can also use the ``pytest_generate_tests`` hook to -implement the `parametrization scheme of your choice`_. See also -:ref:`paramexamples` for more examples. - -.. _`parametrization scheme of your choice`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/ - -pytest interaction with other packages ---------------------------------------------------- - -Issues with pytest, multiprocess and setuptools? -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -On Windows the multiprocess package will instantiate sub processes -by pickling and thus implicitly re-import a lot of local modules. -Unfortunately, setuptools-0.6.11 does not ``if __name__=='__main__'`` -protect its generated command line script. This leads to infinite -recursion when running a test that instantiates Processes. - -As of mid-2013, there shouldn't be a problem anymore when you -use the standard setuptools (note that distribute has been merged -back into setuptools which is now shipped directly with virtualenv). - -.. _nose: https://nose.readthedocs.io/en/latest/ -.. _pylib: https://py.readthedocs.io/en/latest/ -.. _`xUnit style setup`: xunit_setup.html From 36c8bb492e309eb16cb83b9acf85b24b7dc47030 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 12 Aug 2020 17:17:43 -0300 Subject: [PATCH 556/823] get_dirs_from_args handles paths with invalid syntax Fix #7638 --- changelog/7638.bugfix.rst | 1 + src/_pytest/config/findpaths.py | 20 ++++++-------------- testing/test_findpaths.py | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 changelog/7638.bugfix.rst diff --git a/changelog/7638.bugfix.rst b/changelog/7638.bugfix.rst new file mode 100644 index 00000000000..ea3257b6771 --- /dev/null +++ b/changelog/7638.bugfix.rst @@ -0,0 +1 @@ +Fix handling of command-line options that appear as paths but trigger an OS-level syntax error on Windows, such as the options used internally by ``pytest-xdist``. diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 65120e48418..7a2bba5a79f 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -1,6 +1,5 @@ import itertools import os -import sys from typing import Dict from typing import Iterable from typing import List @@ -146,20 +145,13 @@ def get_dir_from_path(path: Path) -> Path: return path return path.parent - if sys.version_info < (3, 8): - - def safe_exists(path: Path) -> bool: - # On Python<3.8, this can throw on paths that contain characters - # unrepresentable at the OS level. - try: - return path.exists() - except OSError: - return False - - else: - - def safe_exists(path: Path) -> bool: + def safe_exists(path: Path) -> bool: + # This can throw on paths that contain characters unrepresentable at the OS level, + # or with invalid syntax on Windows + try: return path.exists() + except OSError: + return False # These look like paths but may not exist possible_paths = ( diff --git a/testing/test_findpaths.py b/testing/test_findpaths.py index acb982b4cf4..974dcf8f3cd 100644 --- a/testing/test_findpaths.py +++ b/testing/test_findpaths.py @@ -2,6 +2,7 @@ import pytest from _pytest.config.findpaths import get_common_ancestor +from _pytest.config.findpaths import get_dirs_from_args from _pytest.config.findpaths import load_config_dict_from_file from _pytest.pathlib import Path @@ -108,3 +109,17 @@ def test_single_file(self, tmp_path: Path) -> None: fn = tmp_path / "foo.py" fn.touch() assert get_common_ancestor([fn]) == tmp_path + + +def test_get_dirs_from_args(tmp_path): + """get_dirs_from_args() skips over non-existing directories and files""" + fn = tmp_path / "foo.py" + fn.touch() + d = tmp_path / "tests" + d.mkdir() + option = "--foobar=/foo.txt" + # xdist uses options in this format for its rsync feature (#7638) + xdist_rsync_option = "popen=c:/dest" + assert get_dirs_from_args( + [str(fn), str(tmp_path / "does_not_exist"), str(d), option, xdist_rsync_option] + ) == [fn.parent, d] From 82181fde3efe01853661af41bac27c312b33a5e7 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 13 Aug 2020 14:18:36 +0200 Subject: [PATCH 557/823] Replace inactive Azure Pipelines badge with GHA Currently the badge encourages you to set it up now. :) --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 00c85ae37ce..042c87b2fec 100644 --- a/README.rst +++ b/README.rst @@ -22,8 +22,8 @@ .. image:: https://travis-ci.org/pytest-dev/pytest.svg?branch=master :target: https://travis-ci.org/pytest-dev/pytest -.. image:: https://dev.azure.com/pytest-dev/pytest/_apis/build/status/pytest-CI?branchName=master - :target: https://dev.azure.com/pytest-dev/pytest +.. image:: https://github.com/pytest-dev/pytest/workflows/main/badge.svg + :target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Amain .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black From 02c6e4455c183f1e3c9c2dacf0904a0d17fd111d Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 14 Aug 2020 08:08:17 +0100 Subject: [PATCH 558/823] document toml use of filterwarnings (#7611) and include a demo of toml 'literal strings' Update doc/en/warnings.rst Apply suggestion by Ran Fix linting Co-authored-by: Bruno Oliveira --- doc/en/warnings.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index d1e27ecad21..fe2ef39dce2 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -68,16 +68,30 @@ them into errors: FAILED test_show_warnings.py::test_one - UserWarning: api v1, should use ... 1 failed in 0.12s -The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option. -For example, the configuration below will ignore all user warnings, but will transform +The same option can be set in the ``pytest.ini`` or ``pyproject.toml`` file using the +``filterwarnings`` ini option. For example, the configuration below will ignore all +user warnings and specific deprecation warnings matching a regex, but will transform all other warnings into errors. .. code-block:: ini + # pytest.ini [pytest] filterwarnings = error ignore::UserWarning + ignore:function ham\(\) is deprecated:DeprecationWarning + +.. code-block:: toml + + # pyproject.toml + [tool.pytest.ini_options] + filterwarnings = [ + "error", + "ignore::UserWarning", + # note the use of single quote below to denote "raw" strings in TOML + 'ignore:function ham\(\) is deprecated:DeprecationWarning', + ] When a warning matches more than one option in the list, the action for the last matching option From 8056a677b4b3c04a9b48b012efc26ea5ab8286e6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 14 Aug 2020 11:01:55 +0300 Subject: [PATCH 559/823] Add changelog for PR #7631 --- changelog/7631.trivial.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog/7631.trivial.rst diff --git a/changelog/7631.trivial.rst b/changelog/7631.trivial.rst new file mode 100644 index 00000000000..81e1d71cc3c --- /dev/null +++ b/changelog/7631.trivial.rst @@ -0,0 +1,2 @@ +The result type of :meth:`capfd.readouterr() <_pytest.capture.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple, +but should behave like one in all respects. This was done for technical reasons. From f28af14457396b2100160985d179aa42f3c1c313 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 14 Aug 2020 13:35:34 +0300 Subject: [PATCH 560/823] Don't use NotImplementedError in `@overload`s We used it as a shortcut for avoiding coverage, but pylint has a special interpretation of it as an abstract method which we don't want. --- .coveragerc | 1 + src/_pytest/_code/code.py | 4 ++-- src/_pytest/_code/source.py | 4 ++-- src/_pytest/compat.py | 4 ++-- src/_pytest/fixtures.py | 4 ++-- src/_pytest/main.py | 8 ++++---- src/_pytest/mark/structures.py | 20 ++++++++++---------- src/_pytest/monkeypatch.py | 4 ++-- src/_pytest/nodes.py | 4 ++-- src/_pytest/pytester.py | 14 +++++++------- src/_pytest/python_api.py | 4 ++-- src/_pytest/recwarn.py | 8 ++++---- src/_pytest/reports.py | 2 +- 13 files changed, 41 insertions(+), 40 deletions(-) diff --git a/.coveragerc b/.coveragerc index b06629a8a8f..09ab3764337 100644 --- a/.coveragerc +++ b/.coveragerc @@ -27,3 +27,4 @@ exclude_lines = ^\s*assert False(,|$) ^\s*if TYPE_CHECKING: + ^\s*@overload( |$) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 420135b4e02..96fa9199b7e 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -334,11 +334,11 @@ def cut( @overload def __getitem__(self, key: int) -> TracebackEntry: - raise NotImplementedError() + ... @overload # noqa: F811 def __getitem__(self, key: slice) -> "Traceback": # noqa: F811 - raise NotImplementedError() + ... def __getitem__( # noqa: F811 self, key: Union[int, slice] diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 8338014ae89..4ba18aa6361 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -44,11 +44,11 @@ def __eq__(self, other: object) -> bool: @overload def __getitem__(self, key: int) -> str: - raise NotImplementedError() + ... @overload # noqa: F811 def __getitem__(self, key: slice) -> "Source": # noqa: F811 - raise NotImplementedError() + ... def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: F811 if isinstance(key, int): diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 4b46d9c950b..0c9f47de707 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -378,13 +378,13 @@ def __init__(self, func: Callable[[_S], _T]) -> None: def __get__( self, instance: None, owner: Optional["Type[_S]"] = ... ) -> "cached_property[_S, _T]": - raise NotImplementedError() + ... @overload # noqa: F811 def __get__( # noqa: F811 self, instance: _S, owner: Optional["Type[_S]"] = ... ) -> _T: - raise NotImplementedError() + ... def __get__(self, instance, owner=None): # noqa: F811 if instance is None: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 846cc2bb132..feb145da075 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1230,7 +1230,7 @@ def fixture( ] = ..., name: Optional[str] = ... ) -> _FixtureFunction: - raise NotImplementedError() + ... @overload # noqa: F811 @@ -1248,7 +1248,7 @@ def fixture( # noqa: F811 ] = ..., name: Optional[str] = None ) -> FixtureFunctionMarker: - raise NotImplementedError() + ... def fixture( # noqa: F811 diff --git a/src/_pytest/main.py b/src/_pytest/main.py index c0e1c9d076e..0baa22a6aaf 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -501,13 +501,13 @@ def gethookproxy(self, fspath: py.path.local): def perform_collect( self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ... ) -> Sequence[nodes.Item]: - raise NotImplementedError() + ... @overload # noqa: F811 def perform_collect( # noqa: F811 self, args: Optional[Sequence[str]] = ..., genitems: bool = ... ) -> Sequence[Union[nodes.Item, nodes.Collector]]: - raise NotImplementedError() + ... def perform_collect( # noqa: F811 self, args: Optional[Sequence[str]] = None, genitems: bool = True @@ -528,13 +528,13 @@ def perform_collect( # noqa: F811 def _perform_collect( self, args: Optional[Sequence[str]], genitems: "Literal[True]" ) -> List[nodes.Item]: - raise NotImplementedError() + ... @overload # noqa: F811 def _perform_collect( # noqa: F811 self, args: Optional[Sequence[str]], genitems: bool ) -> Union[List[Union[nodes.Item]], List[Union[nodes.Item, nodes.Collector]]]: - raise NotImplementedError() + ... def _perform_collect( # noqa: F811 self, args: Optional[Sequence[str]], genitems: bool diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index ea1ba546c3f..73e1f77ce74 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -327,13 +327,13 @@ def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": # the first match so it works out even if we break the rules. @overload def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc] - raise NotImplementedError() + pass @overload # noqa: F811 def __call__( # noqa: F811 self, *args: object, **kwargs: object ) -> "MarkDecorator": - raise NotImplementedError() + pass def __call__(self, *args: object, **kwargs: object): # noqa: F811 """Call the MarkDecorator.""" @@ -388,11 +388,11 @@ def store_mark(obj, mark: Mark) -> None: class _SkipMarkDecorator(MarkDecorator): @overload # type: ignore[override,misc] def __call__(self, arg: _Markable) -> _Markable: - raise NotImplementedError() + ... @overload # noqa: F811 def __call__(self, reason: str = ...) -> "MarkDecorator": # noqa: F811 - raise NotImplementedError() + ... class _SkipifMarkDecorator(MarkDecorator): def __call__( # type: ignore[override] @@ -401,12 +401,12 @@ def __call__( # type: ignore[override] *conditions: Union[str, bool], reason: str = ... ) -> MarkDecorator: - raise NotImplementedError() + ... class _XfailMarkDecorator(MarkDecorator): @overload # type: ignore[override,misc] def __call__(self, arg: _Markable) -> _Markable: - raise NotImplementedError() + ... @overload # noqa: F811 def __call__( # noqa: F811 @@ -420,7 +420,7 @@ def __call__( # noqa: F811 ] = ..., strict: bool = ... ) -> MarkDecorator: - raise NotImplementedError() + ... class _ParametrizeMarkDecorator(MarkDecorator): def __call__( # type: ignore[override] @@ -437,19 +437,19 @@ def __call__( # type: ignore[override] ] = ..., scope: Optional[_Scope] = ... ) -> MarkDecorator: - raise NotImplementedError() + ... class _UsefixturesMarkDecorator(MarkDecorator): def __call__( # type: ignore[override] self, *fixtures: str ) -> MarkDecorator: - raise NotImplementedError() + ... class _FilterwarningsMarkDecorator(MarkDecorator): def __call__( # type: ignore[override] self, *filters: str ) -> MarkDecorator: - raise NotImplementedError() + ... class MarkGenerator: diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 4a4dd67a135..1f324986b68 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -152,13 +152,13 @@ def test_partial(monkeypatch): def setattr( self, target: str, name: object, value: Notset = ..., raising: bool = ..., ) -> None: - raise NotImplementedError() + ... @overload # noqa: F811 def setattr( # noqa: F811 self, target: object, name: str, value: object, raising: bool = ..., ) -> None: - raise NotImplementedError() + ... def setattr( # noqa: F811 self, diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 9522f418472..9d2365c4d5e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -311,11 +311,11 @@ def iter_markers_with_node( @overload def get_closest_marker(self, name: str) -> Optional[Mark]: - raise NotImplementedError() + ... @overload # noqa: F811 def get_closest_marker(self, name: str, default: Mark) -> Mark: # noqa: F811 - raise NotImplementedError() + ... def get_closest_marker( # noqa: F811 self, name: str, default: Optional[Mark] = None diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 83c525fd8c1..5d8a45ad702 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -201,7 +201,7 @@ def __repr__(self) -> str: if TYPE_CHECKING: # The class has undetermined attributes, this tells mypy about it. def __getattr__(self, key: str): - raise NotImplementedError() + ... class HookRecorder: @@ -274,13 +274,13 @@ def getcall(self, name: str) -> ParsedCall: def getreports( self, names: "Literal['pytest_collectreport']", ) -> Sequence[CollectReport]: - raise NotImplementedError() + ... @overload # noqa: F811 def getreports( # noqa: F811 self, names: "Literal['pytest_runtest_logreport']", ) -> Sequence[TestReport]: - raise NotImplementedError() + ... @overload # noqa: F811 def getreports( # noqa: F811 @@ -290,7 +290,7 @@ def getreports( # noqa: F811 "pytest_runtest_logreport", ), ) -> Sequence[Union[CollectReport, TestReport]]: - raise NotImplementedError() + ... def getreports( # noqa: F811 self, @@ -337,13 +337,13 @@ def matchreport( def getfailures( self, names: "Literal['pytest_collectreport']", ) -> Sequence[CollectReport]: - raise NotImplementedError() + ... @overload # noqa: F811 def getfailures( # noqa: F811 self, names: "Literal['pytest_runtest_logreport']", ) -> Sequence[TestReport]: - raise NotImplementedError() + ... @overload # noqa: F811 def getfailures( # noqa: F811 @@ -353,7 +353,7 @@ def getfailures( # noqa: F811 "pytest_runtest_logreport", ), ) -> Sequence[Union[CollectReport, TestReport]]: - raise NotImplementedError() + ... def getfailures( # noqa: F811 self, diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index c0c266cbd18..a1eb29e1aba 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -529,7 +529,7 @@ def raises( *, match: "Optional[Union[str, Pattern[str]]]" = ... ) -> "RaisesContext[_E]": - ... # pragma: no cover + ... @overload # noqa: F811 @@ -539,7 +539,7 @@ def raises( # noqa: F811 *args: Any, **kwargs: Any ) -> _pytest._code.ExceptionInfo[_E]: - ... # pragma: no cover + ... def raises( # noqa: F811 diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 7eb7020d02f..3668de627e6 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -42,14 +42,14 @@ def recwarn() -> Generator["WarningsRecorder", None, None]: def deprecated_call( *, match: Optional[Union[str, "Pattern[str]"]] = ... ) -> "WarningsRecorder": - raise NotImplementedError() + ... @overload # noqa: F811 def deprecated_call( # noqa: F811 func: Callable[..., T], *args: Any, **kwargs: Any ) -> T: - raise NotImplementedError() + ... def deprecated_call( # noqa: F811 @@ -89,7 +89,7 @@ def warns( *, match: "Optional[Union[str, Pattern[str]]]" = ... ) -> "WarningsChecker": - raise NotImplementedError() + ... @overload # noqa: F811 @@ -99,7 +99,7 @@ def warns( # noqa: F811 *args: Any, **kwargs: Any ) -> T: - raise NotImplementedError() + ... def warns( # noqa: F811 diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 3053798634c..48caa6ceebe 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -71,7 +71,7 @@ def __init__(self, **kw: Any) -> None: if TYPE_CHECKING: # Can have arbitrary fields given to __init__(). def __getattr__(self, key: str) -> Any: - raise NotImplementedError() + ... def toterminal(self, out: TerminalWriter) -> None: if hasattr(self, "node"): From f76b1622632acc7b19faf45502564f2d559823ff Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 14 Aug 2020 11:36:07 -0300 Subject: [PATCH 561/823] Add ref to Python bug --- src/_pytest/config/findpaths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 7a2bba5a79f..facf30a87a2 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -147,7 +147,7 @@ def get_dir_from_path(path: Path) -> Path: def safe_exists(path: Path) -> bool: # This can throw on paths that contain characters unrepresentable at the OS level, - # or with invalid syntax on Windows + # or with invalid syntax on Windows (https://bugs.python.org/issue35306) try: return path.exists() except OSError: From eddd993cf469df33268097f4bdaf60ccb8f50d3b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 15 Aug 2020 11:35:54 +0300 Subject: [PATCH 562/823] Only define gethookproxy, isinitpath on Session This fixes an issue where pylint complains about missing implementations of abstract methods in subclasses of `File` which only override `collect()` (as they should). It is also cleaner and makes sense, these methods really don't need to be overridden. The previous methods defined directly on `FSCollector` and `Package` are deprecated, to be removed in pytest 7. See commits e2934c3f8c03c83469f4c6670c207773a6e02df4 and f10ab021e21a44e2f0fa2be66660c4a6d4b7a61a for reference. --- changelog/7591.bugfix.rst | 1 + changelog/7648.deprecation.rst | 3 +++ src/_pytest/deprecated.py | 5 ++++ src/_pytest/main.py | 27 ++++++++++++++++++- src/_pytest/nodes.py | 47 ++++++++-------------------------- src/_pytest/python.py | 7 +++-- testing/deprecated_test.py | 27 +++++++++++++++++++ 7 files changed, 78 insertions(+), 39 deletions(-) create mode 100644 changelog/7591.bugfix.rst create mode 100644 changelog/7648.deprecation.rst diff --git a/changelog/7591.bugfix.rst b/changelog/7591.bugfix.rst new file mode 100644 index 00000000000..10de43a96a5 --- /dev/null +++ b/changelog/7591.bugfix.rst @@ -0,0 +1 @@ +pylint shouldn't complain anymore about unimplemented abstract methods when inheriting from :ref:`File `. diff --git a/changelog/7648.deprecation.rst b/changelog/7648.deprecation.rst new file mode 100644 index 00000000000..440b1114116 --- /dev/null +++ b/changelog/7648.deprecation.rst @@ -0,0 +1,3 @@ +The ``gethookproxy()`` and ``isinitpath()`` methods of ``FSCollector`` and ``Package`` are deprecated; +use ``self.session.gethookproxy()`` and ``self.session.isinitpath()`` instead. +This should work on all pytest versions. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index bd2574ba769..7481473fdae 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -84,3 +84,8 @@ "The pytest_warning_captured is deprecated and will be removed in a future release.\n" "Please use pytest_warning_recorded instead." ) + +FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestDeprecationWarning( + "The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; " + "use self.session.gethookproxy() and self.session.isinitpath() instead. " +) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 0baa22a6aaf..58d45ebd196 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -27,6 +27,7 @@ from _pytest.config import directory_arg from _pytest.config import ExitCode from _pytest.config import hookimpl +from _pytest.config import PytestPluginManager from _pytest.config import UsageError from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureManager @@ -389,6 +390,17 @@ def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> No items[:] = remaining +class FSHookProxy: + def __init__(self, pm: PytestPluginManager, remove_mods) -> None: + self.pm = pm + self.remove_mods = remove_mods + + def __getattr__(self, name: str): + x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) + self.__dict__[name] = x + return x + + class NoMatch(Exception): """Matching cannot locate matching names.""" @@ -495,7 +507,20 @@ def isinitpath(self, path: py.path.local) -> bool: return path in self._initialpaths def gethookproxy(self, fspath: py.path.local): - return super()._gethookproxy(fspath) + # Check if we have the common case of running + # hooks with all conftest.py files. + pm = self.config.pluginmanager + my_conftestmodules = pm._getconftestmodules( + fspath, self.config.getoption("importmode") + ) + remove_mods = pm._conftest_plugins.difference(my_conftestmodules) + if remove_mods: + # One or more conftests are not in use at this fspath. + proxy = FSHookProxy(pm, remove_mods) + else: + # All plugins are active for this fspath. + proxy = self.config.hook + return proxy @overload def perform_collect( diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 9d2365c4d5e..79e0914438b 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -25,7 +25,7 @@ from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import ConftestImportFailure -from _pytest.config import PytestPluginManager +from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.deprecated import NODE_USE_FROM_PARENT from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError @@ -495,17 +495,6 @@ def _check_initialpaths_for_relpath(session, fspath): return fspath.relto(initial_path) -class FSHookProxy: - def __init__(self, pm: PytestPluginManager, remove_mods) -> None: - self.pm = pm - self.remove_mods = remove_mods - - def __getattr__(self, name: str): - x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) - self.__dict__[name] = x - return x - - class FSCollector(Collector): def __init__( self, @@ -542,42 +531,28 @@ def from_parent(cls, parent, *, fspath, **kw): """The public constructor.""" return super().from_parent(parent=parent, fspath=fspath, **kw) - def _gethookproxy(self, fspath: py.path.local): - # Check if we have the common case of running - # hooks with all conftest.py files. - pm = self.config.pluginmanager - my_conftestmodules = pm._getconftestmodules( - fspath, self.config.getoption("importmode") - ) - remove_mods = pm._conftest_plugins.difference(my_conftestmodules) - if remove_mods: - # One or more conftests are not in use at this fspath. - proxy = FSHookProxy(pm, remove_mods) - else: - # All plugins are active for this fspath. - proxy = self.config.hook - return proxy - def gethookproxy(self, fspath: py.path.local): - raise NotImplementedError() + warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) + return self.session.gethookproxy(fspath) + + def isinitpath(self, path: py.path.local) -> bool: + warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) + return self.session.isinitpath(path) def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if direntry.name == "__pycache__": return False path = py.path.local(direntry.path) - ihook = self._gethookproxy(path.dirpath()) + ihook = self.session.gethookproxy(path.dirpath()) if ihook.pytest_ignore_collect(path=path, config=self.config): return False for pat in self._norecursepatterns: if path.check(fnmatch=pat): return False - ihook = self._gethookproxy(path) + ihook = self.session.gethookproxy(path) ihook.pytest_collect_directory(path=path, parent=self) return True - def isinitpath(self, path: py.path.local) -> bool: - raise NotImplementedError() - def _collectfile( self, path: py.path.local, handle_dupes: bool = True ) -> Sequence[Collector]: @@ -586,8 +561,8 @@ def _collectfile( ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( path, path.isdir(), path.exists(), path.islink() ) - ihook = self.gethookproxy(path) - if not self.isinitpath(path): + ihook = self.session.gethookproxy(path) + if not self.session.isinitpath(path): if ihook.pytest_ignore_collect(path=path, config=self.config): return () diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 0661f340209..820b7e86c91 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -52,6 +52,7 @@ from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config.argparsing import Parser +from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.deprecated import FUNCARGNAMES from _pytest.fixtures import FuncFixtureInfo from _pytest.main import Session @@ -627,10 +628,12 @@ def setup(self) -> None: self.addfinalizer(func) def gethookproxy(self, fspath: py.path.local): - return super()._gethookproxy(fspath) + warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) + return self.session.gethookproxy(fspath) def isinitpath(self, path: py.path.local) -> bool: - return path in self.session._initialpaths + warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) + return self.session.isinitpath(path) def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: this_path = self.fspath.dirpath() diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index f4de3b5d9c5..9507d990230 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,11 +1,13 @@ import copy import inspect +import warnings from unittest import mock import pytest from _pytest import deprecated from _pytest import nodes from _pytest.config import Config +from _pytest.pytester import Testdir @pytest.mark.filterwarnings("default") @@ -151,3 +153,28 @@ def test_three(): assert 1 ) result = testdir.runpytest("-k", "test_two:", threepass) result.stdout.fnmatch_lines(["*The `-k 'expr:'` syntax*deprecated*"]) + + +def test_fscollector_gethookproxy_isinitpath(testdir: Testdir) -> None: + module = testdir.getmodulecol( + """ + def test_foo(): pass + """, + withinit=True, + ) + assert isinstance(module, pytest.Module) + package = module.parent + assert isinstance(package, pytest.Package) + + with pytest.warns(pytest.PytestDeprecationWarning, match="gethookproxy"): + package.gethookproxy(testdir.tmpdir) + + with pytest.warns(pytest.PytestDeprecationWarning, match="isinitpath"): + package.isinitpath(testdir.tmpdir) + + # The methods on Session are *not* deprecated. + session = module.session + with warnings.catch_warnings(record=True) as rec: + session.gethookproxy(testdir.tmpdir) + session.isinitpath(testdir.tmpdir) + assert len(rec) == 0 From 2213016e40176cdcb1397ad03499ed1881d89ac9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Aug 2020 21:37:59 -0300 Subject: [PATCH 563/823] Fix Module.name from full path without drive letter Fix #7628 --- changelog/7628.bugfix.rst | 1 + src/_pytest/main.py | 9 +++++---- testing/test_collection.py | 39 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 changelog/7628.bugfix.rst diff --git a/changelog/7628.bugfix.rst b/changelog/7628.bugfix.rst new file mode 100644 index 00000000000..9f3480aaa65 --- /dev/null +++ b/changelog/7628.bugfix.rst @@ -0,0 +1 @@ +Fix test collection when a full path without a drive letter was passed to pytest on Windows (for example ``\projects\tests\test.py`` instead of ``c:\projects\tests\pytest.py``). diff --git a/src/_pytest/main.py b/src/_pytest/main.py index c0e1c9d076e..e0ca1c0b58a 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -31,6 +31,7 @@ from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit +from _pytest.pathlib import absolutepath from _pytest.pathlib import Path from _pytest.pathlib import visit from _pytest.reports import CollectReport @@ -693,15 +694,15 @@ def _parsearg(self, arg: str) -> Tuple[py.path.local, List[str]]: strpath, *parts = str(arg).split("::") if self.config.option.pyargs: strpath = self._tryconvertpyarg(strpath) - relpath = strpath.replace("/", os.sep) - fspath = self.config.invocation_dir.join(relpath, abs=True) - if not fspath.check(): + fspath = Path(str(self.config.invocation_dir), strpath) + fspath = absolutepath(fspath) + if not fspath.exists(): if self.config.option.pyargs: raise UsageError( "file or package not found: " + arg + " (missing __init__.py?)" ) raise UsageError("file not found: " + arg) - return (fspath, parts) + return py.path.local(str(fspath)), parts def matchnodes( self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str], diff --git a/testing/test_collection.py b/testing/test_collection.py index 9f22f3ee038..283df082f7b 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1426,3 +1426,42 @@ def test_modules_not_importable_as_side_effect(self, testdir): "* 1 failed in *", ] ) + + +def test_module_full_path_without_drive(testdir): + """Collect and run test using full path except for the drive letter (#7628) + + Passing a full path without a drive letter would trigger a bug in py.path.local + where it would keep the full path without the drive letter around, instead of resolving + to the full path, resulting in fixtures node ids not matching against test node ids correctly. + """ + testdir.makepyfile( + **{ + "project/conftest.py": """ + import pytest + @pytest.fixture + def fix(): return 1 + """, + } + ) + + testdir.makepyfile( + **{ + "project/tests/dummy_test.py": """ + def test(fix): + assert fix == 1 + """ + } + ) + fn = testdir.tmpdir.join("project/tests/dummy_test.py") + assert fn.isfile() + + drive, path = os.path.splitdrive(str(fn)) + + result = testdir.runpytest(path, "-v") + result.stdout.fnmatch_lines( + [ + os.path.join("project", "tests", "dummy_test.py") + "::test PASSED *", + "* 1 passed in *", + ] + ) From b426bb3443b0895f8a84379ff4a9949330012d38 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 15 Aug 2020 10:31:17 -0300 Subject: [PATCH 564/823] Refactor Session._parsearg into a separate function for testing --- src/_pytest/main.py | 91 +++++++++++++++--------- testing/acceptance_test.py | 6 +- testing/test_collection.py | 58 --------------- testing/test_main.py | 141 +++++++++++++++++++++++++++++++++++++ testing/test_terminal.py | 4 +- 5 files changed, 206 insertions(+), 94 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index e0ca1c0b58a..9df9d33b983 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -549,7 +549,9 @@ def _perform_collect( # noqa: F811 self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]] self.items = items = [] # type: List[nodes.Item] for arg in args: - fspath, parts = self._parsearg(arg) + fspath, parts = resolve_collection_argument( + self.config.invocation_dir, arg, as_pypath=self.config.option.pyargs + ) self._initial_parts.append((fspath, parts)) initialpaths.append(fspath) self._initialpaths = frozenset(initialpaths) @@ -673,37 +675,6 @@ def _collect( return yield from m - def _tryconvertpyarg(self, x: str) -> str: - """Convert a dotted module name to path.""" - try: - spec = importlib.util.find_spec(x) - # AttributeError: looks like package module, but actually filename - # ImportError: module does not exist - # ValueError: not a module name - except (AttributeError, ImportError, ValueError): - return x - if spec is None or spec.origin is None or spec.origin == "namespace": - return x - elif spec.submodule_search_locations: - return os.path.dirname(spec.origin) - else: - return spec.origin - - def _parsearg(self, arg: str) -> Tuple[py.path.local, List[str]]: - """Return (fspath, names) tuple after checking the file exists.""" - strpath, *parts = str(arg).split("::") - if self.config.option.pyargs: - strpath = self._tryconvertpyarg(strpath) - fspath = Path(str(self.config.invocation_dir), strpath) - fspath = absolutepath(fspath) - if not fspath.exists(): - if self.config.option.pyargs: - raise UsageError( - "file or package not found: " + arg + " (missing __init__.py?)" - ) - raise UsageError("file not found: " + arg) - return py.path.local(str(fspath)), parts - def matchnodes( self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str], ) -> Sequence[Union[nodes.Item, nodes.Collector]]: @@ -770,3 +741,59 @@ def genitems( for subnode in rep.result: yield from self.genitems(subnode) node.ihook.pytest_collectreport(report=rep) + + +def search_pypath(module_name: str) -> str: + """Search sys.path for the given a dotted module name, and return its file system path.""" + try: + spec = importlib.util.find_spec(module_name) + # AttributeError: looks like package module, but actually filename + # ImportError: module does not exist + # ValueError: not a module name + except (AttributeError, ImportError, ValueError): + return module_name + if spec is None or spec.origin is None or spec.origin == "namespace": + return module_name + elif spec.submodule_search_locations: + return os.path.dirname(spec.origin) + else: + return spec.origin + + +def resolve_collection_argument( + invocation_dir: py.path.local, arg: str, *, as_pypath: bool = False +) -> Tuple[py.path.local, List[str]]: + """Parse path arguments optionally containing selection parts and return (fspath, names). + + Command-line arguments can point to files and/or directories, and optionally contain + parts for specific tests selection, for example: + + "pkg/tests/test_foo.py::TestClass::test_foo" + + This function ensures the path exists, and returns a tuple: + + (py.path.path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"]) + + When as_pypath is True, expects that the command-line argument actually contains + module paths instead of file-system paths: + + "pkg.tests.test_foo::TestClass::test_foo" + + In which case we search sys.path for a matching module, and then return the *path* to the + found module. + + If the path doesn't exist, raise UsageError. + """ + strpath, *parts = str(arg).split("::") + if as_pypath: + strpath = search_pypath(strpath) + fspath = Path(str(invocation_dir), strpath) + fspath = absolutepath(fspath) + if not fspath.exists(): + msg = ( + "module or package not found: {arg} (missing __init__.py?)" + if as_pypath + else "file or directory not found: {arg}" + ) + raise UsageError(msg.format(arg=arg)) + return py.path.local(str(fspath)), parts diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index b37cfa0cbd9..d58b4fd030c 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -70,7 +70,7 @@ def pytest_configure(): def test_file_not_found(self, testdir): result = testdir.runpytest("asd") assert result.ret != 0 - result.stderr.fnmatch_lines(["ERROR: file not found*asd"]) + result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"]) def test_file_not_found_unconfigure_issue143(self, testdir): testdir.makeconftest( @@ -83,7 +83,7 @@ def pytest_unconfigure(): ) result = testdir.runpytest("-s", "asd") assert result.ret == ExitCode.USAGE_ERROR - result.stderr.fnmatch_lines(["ERROR: file not found*asd"]) + result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"]) result.stdout.fnmatch_lines(["*---configure", "*---unconfigure"]) def test_config_preparse_plugin_option(self, testdir): @@ -791,7 +791,7 @@ def test_cmdline_python_package_symlink(self, testdir, monkeypatch): def test_cmdline_python_package_not_exists(self, testdir): result = testdir.runpytest("--pyargs", "tpkgwhatv") assert result.ret - result.stderr.fnmatch_lines(["ERROR*file*or*package*not*found*"]) + result.stderr.fnmatch_lines(["ERROR*module*or*package*not*found*"]) @pytest.mark.xfail(reason="decide: feature or bug") def test_noclass_discovery_if_not_testcase(self, testdir): diff --git a/testing/test_collection.py b/testing/test_collection.py index 283df082f7b..01dbcc73130 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -443,25 +443,6 @@ def pytest_collect_file(path, parent): class TestSession: - def test_parsearg(self, testdir) -> None: - p = testdir.makepyfile("def test_func(): pass") - subdir = testdir.mkdir("sub") - subdir.ensure("__init__.py") - target = subdir.join(p.basename) - p.move(target) - subdir.chdir() - config = testdir.parseconfig(p.basename) - rcol = Session.from_config(config) - assert rcol.fspath == subdir - fspath, parts = rcol._parsearg(p.basename) - - assert fspath == target - assert len(parts) == 0 - fspath, parts = rcol._parsearg(p.basename + "::test_func") - assert fspath == target - assert parts[0] == "test_func" - assert len(parts) == 1 - def test_collect_topdir(self, testdir): p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) @@ -1426,42 +1407,3 @@ def test_modules_not_importable_as_side_effect(self, testdir): "* 1 failed in *", ] ) - - -def test_module_full_path_without_drive(testdir): - """Collect and run test using full path except for the drive letter (#7628) - - Passing a full path without a drive letter would trigger a bug in py.path.local - where it would keep the full path without the drive letter around, instead of resolving - to the full path, resulting in fixtures node ids not matching against test node ids correctly. - """ - testdir.makepyfile( - **{ - "project/conftest.py": """ - import pytest - @pytest.fixture - def fix(): return 1 - """, - } - ) - - testdir.makepyfile( - **{ - "project/tests/dummy_test.py": """ - def test(fix): - assert fix == 1 - """ - } - ) - fn = testdir.tmpdir.join("project/tests/dummy_test.py") - assert fn.isfile() - - drive, path = os.path.splitdrive(str(fn)) - - result = testdir.runpytest(path, "-v") - result.stdout.fnmatch_lines( - [ - os.path.join("project", "tests", "dummy_test.py") + "::test PASSED *", - "* 1 passed in *", - ] - ) diff --git a/testing/test_main.py b/testing/test_main.py index ee8349a9f33..4546c83ba23 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -1,8 +1,14 @@ import argparse +import os +import re from typing import Optional +import py.path + import pytest from _pytest.config import ExitCode +from _pytest.config import UsageError +from _pytest.main import resolve_collection_argument from _pytest.main import validate_basetemp from _pytest.pytester import Testdir @@ -98,3 +104,138 @@ def test_validate_basetemp_fails(tmp_path, basetemp, monkeypatch): def test_validate_basetemp_integration(testdir): result = testdir.runpytest("--basetemp=.") result.stderr.fnmatch_lines("*basetemp must not be*") + + +class TestResolveCollectionArgument: + @pytest.fixture + def root(self, testdir): + testdir.syspathinsert(str(testdir.tmpdir / "src")) + testdir.chdir() + + pkg = testdir.tmpdir.join("src/pkg").ensure_dir() + pkg.join("__init__.py").ensure(file=True) + pkg.join("test.py").ensure(file=True) + return testdir.tmpdir + + def test_file(self, root): + """File and parts.""" + assert resolve_collection_argument(root, "src/pkg/test.py") == ( + root / "src/pkg/test.py", + [], + ) + assert resolve_collection_argument(root, "src/pkg/test.py::") == ( + root / "src/pkg/test.py", + [""], + ) + assert resolve_collection_argument(root, "src/pkg/test.py::foo::bar") == ( + root / "src/pkg/test.py", + ["foo", "bar"], + ) + assert resolve_collection_argument(root, "src/pkg/test.py::foo::bar::") == ( + root / "src/pkg/test.py", + ["foo", "bar", ""], + ) + + def test_dir(self, root): + """Directory and parts.""" + assert resolve_collection_argument(root, "src/pkg") == (root / "src/pkg", []) + assert resolve_collection_argument(root, "src/pkg::") == ( + root / "src/pkg", + [""], + ) + assert resolve_collection_argument(root, "src/pkg::foo::bar") == ( + root / "src/pkg", + ["foo", "bar"], + ) + assert resolve_collection_argument(root, "src/pkg::foo::bar::") == ( + root / "src/pkg", + ["foo", "bar", ""], + ) + + def test_pypath(self, root): + """Dotted name and parts.""" + assert resolve_collection_argument(root, "pkg.test", as_pypath=True) == ( + root / "src/pkg/test.py", + [], + ) + assert resolve_collection_argument( + root, "pkg.test::foo::bar", as_pypath=True + ) == (root / "src/pkg/test.py", ["foo", "bar"],) + assert resolve_collection_argument(root, "pkg", as_pypath=True) == ( + root / "src/pkg", + [], + ) + assert resolve_collection_argument(root, "pkg::foo::bar", as_pypath=True) == ( + root / "src/pkg", + ["foo", "bar"], + ) + + def test_does_not_exist(self, root): + """Given a file/module that does not exist raises UsageError.""" + with pytest.raises( + UsageError, match=re.escape("file or directory not found: foobar") + ): + resolve_collection_argument(root, "foobar") + + with pytest.raises( + UsageError, + match=re.escape( + "module or package not found: foobar (missing __init__.py?)" + ), + ): + resolve_collection_argument(root, "foobar", as_pypath=True) + + def test_absolute_paths_are_resolved_correctly(self, root): + """Absolute paths resolve back to absolute paths.""" + full_path = str(root / "src") + assert resolve_collection_argument(root, full_path) == ( + py.path.local(os.path.abspath("src")), + [], + ) + + # ensure full paths given in the command-line without the drive letter resolve + # to the full path correctly (#7628) + drive, full_path_without_drive = os.path.splitdrive(full_path) + assert resolve_collection_argument(root, full_path_without_drive) == ( + py.path.local(os.path.abspath("src")), + [], + ) + + +def test_module_full_path_without_drive(testdir): + """Collect and run test using full path except for the drive letter (#7628). + + Passing a full path without a drive letter would trigger a bug in py.path.local + where it would keep the full path without the drive letter around, instead of resolving + to the full path, resulting in fixtures node ids not matching against test node ids correctly. + """ + testdir.makepyfile( + **{ + "project/conftest.py": """ + import pytest + @pytest.fixture + def fix(): return 1 + """, + } + ) + + testdir.makepyfile( + **{ + "project/tests/dummy_test.py": """ + def test(fix): + assert fix == 1 + """ + } + ) + fn = testdir.tmpdir.join("project/tests/dummy_test.py") + assert fn.isfile() + + drive, path = os.path.splitdrive(str(fn)) + + result = testdir.runpytest(path, "-v") + result.stdout.fnmatch_lines( + [ + os.path.join("project", "tests", "dummy_test.py") + "::test PASSED *", + "* 1 passed in *", + ] + ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 36604ece78a..0e26ae13c78 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -442,7 +442,9 @@ def test_collectonly_missing_path(self, testdir): have the items attribute.""" result = testdir.runpytest("--collect-only", "uhm_missing_path") assert result.ret == 4 - result.stderr.fnmatch_lines(["*ERROR: file not found*"]) + result.stderr.fnmatch_lines( + ["*ERROR: file or directory not found: uhm_missing_path"] + ) def test_collectonly_quiet(self, testdir): testdir.makepyfile("def test_foo(): pass") From 0d5a65091d45e04662837ace13dd88dba954ea8a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 15 Aug 2020 19:16:59 +0300 Subject: [PATCH 565/823] capture: fix disabled()/global_and_fixture_disabled() enabling capturing when it was disabled The `CaptureManager.global_and_fixture_disabled()` context manager (and `CaptureFixture.disabled()` which calls it) did `suspend(); ...; resume()` but if the capturing was already suspended, the `resume()` would resume it when it shouldn't. This caused caused some messages to be swallowed when `--log-cli` is used because it uses `global_and_fixture_disabled` when capturing is not necessarily resumed. --- changelog/7148.bugfix.rst | 1 + src/_pytest/capture.py | 24 +++++++++++++++++++++--- testing/test_capture.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 changelog/7148.bugfix.rst diff --git a/changelog/7148.bugfix.rst b/changelog/7148.bugfix.rst new file mode 100644 index 00000000000..71753334c51 --- /dev/null +++ b/changelog/7148.bugfix.rst @@ -0,0 +1 @@ +Fixed ``--log-cli`` potentially causing unrelated ``print`` output to be swallowed. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 7daf36dddbb..3bf3bc923ef 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -592,7 +592,7 @@ def suspend_capturing(self, in_: bool = False) -> None: self._in_suspended = True def resume_capturing(self) -> None: - self._state = "resumed" + self._state = "started" if self.out: self.out.resume() if self.err: @@ -613,6 +613,10 @@ def stop_capturing(self) -> None: if self.in_: self.in_.done() + def is_started(self) -> bool: + """Whether actively capturing -- not suspended or stopped.""" + return self._state == "started" + def readouterr(self) -> CaptureResult[AnyStr]: if self.out: out = self.out.snap() @@ -757,11 +761,19 @@ def resume_fixture(self) -> None: @contextlib.contextmanager def global_and_fixture_disabled(self) -> Generator[None, None, None]: """Context manager to temporarily disable global and current fixture capturing.""" - self.suspend() + do_fixture = self._capture_fixture and self._capture_fixture._is_started() + if do_fixture: + self.suspend_fixture() + do_global = self._global_capturing and self._global_capturing.is_started() + if do_global: + self.suspend_global_capture() try: yield finally: - self.resume() + if do_global: + self.resume_global_capture() + if do_fixture: + self.resume_fixture() @contextlib.contextmanager def item_capture(self, when: str, item: Item) -> Generator[None, None, None]: @@ -871,6 +883,12 @@ def _resume(self) -> None: if self._capture is not None: self._capture.resume_capturing() + def _is_started(self) -> bool: + """Whether actively capturing -- not disabled or closed.""" + if self._capture is not None: + return self._capture.is_started() + return False + @contextlib.contextmanager def disabled(self) -> Generator[None, None, None]: """Temporarily disable capturing while inside the ``with`` block.""" diff --git a/testing/test_capture.py b/testing/test_capture.py index b7545d73f76..5f820d8465b 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -17,6 +17,7 @@ from _pytest.capture import CaptureResult from _pytest.capture import MultiCapture from _pytest.config import ExitCode +from _pytest.pytester import Testdir # note: py.io capture tests where copied from # pylib 1.4.20.dev2 (rev 13d9af95547e) @@ -640,6 +641,34 @@ def test_normal(): else: result.stdout.no_fnmatch_line("*test_normal executed*") + def test_disabled_capture_fixture_twice(self, testdir: Testdir) -> None: + """Test that an inner disabled() exit doesn't undo an outer disabled(). + + Issue #7148. + """ + testdir.makepyfile( + """ + def test_disabled(capfd): + print('captured before') + with capfd.disabled(): + print('while capture is disabled 1') + with capfd.disabled(): + print('while capture is disabled 2') + print('while capture is disabled 1 after') + print('captured after') + assert capfd.readouterr() == ('captured before\\ncaptured after\\n', '') + """ + ) + result = testdir.runpytest_subprocess() + result.stdout.fnmatch_lines( + [ + "*while capture is disabled 1", + "*while capture is disabled 2", + "*while capture is disabled 1 after", + ], + consecutive=True, + ) + @pytest.mark.parametrize("fixture", ["capsys", "capfd"]) def test_fixture_use_by_other_fixtures(self, testdir, fixture): """Ensure that capsys and capfd can be used by other fixtures during From 98530184a5d8f6b84da9cd1a72907f6ea28a65be Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 17 Aug 2020 17:11:16 -0300 Subject: [PATCH 566/823] Remove funcargnames compatibility property --- changelog/5585.breaking.rst | 4 ++++ doc/en/deprecations.rst | 27 +++++++++++++-------------- src/_pytest/deprecated.py | 4 ---- src/_pytest/fixtures.py | 7 ------- src/_pytest/python.py | 13 ------------- testing/python/fixtures.py | 22 ---------------------- 6 files changed, 17 insertions(+), 60 deletions(-) create mode 100644 changelog/5585.breaking.rst diff --git a/changelog/5585.breaking.rst b/changelog/5585.breaking.rst new file mode 100644 index 00000000000..12b9a52dd8b --- /dev/null +++ b/changelog/5585.breaking.rst @@ -0,0 +1,4 @@ +As per our policy, the following features have been deprecated in the 5.X series and are now +removed: + +* The ``funcargnames`` read-only property of ``FixtureRequest``, ``Metafunc``, and ``Function`` classes. Use ``fixturenames`` attribute. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 3334b5d5fe4..d5c7540edff 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -127,20 +127,6 @@ Services known to support the ``xunit2`` format: * `Azure Pipelines `__. -``funcargnames`` alias for ``fixturenames`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.0 - -The ``FixtureRequest``, ``Metafunc``, and ``Function`` classes track the names of -their associated fixtures, with the aptly-named ``fixturenames`` attribute. - -Prior to pytest 2.3, this attribute was named ``funcargnames``, and we have kept -that as an alias since. It is finally due for removal, as it is often confusing -in places where we or plugin authors must distinguish between fixture names and -names supplied by non-fixture things such as ``pytest.mark.parametrize``. - - Result log (``--result-log``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -176,6 +162,19 @@ Removed Features As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after an appropriate period of deprecation has passed. +``funcargnames`` alias for ``fixturenames`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 6.0 + +The ``FixtureRequest``, ``Metafunc``, and ``Function`` classes track the names of +their associated fixtures, with the aptly-named ``fixturenames`` attribute. + +Prior to pytest 2.3, this attribute was named ``funcargnames``, and we have kept +that as an alias since. It is finally due for removal, as it is often confusing +in places where we or plugin authors must distinguish between fixture names and +names supplied by non-fixture things such as ``pytest.mark.parametrize``. + ``pytest.config`` global ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 7481473fdae..d76144d6007 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -19,10 +19,6 @@ "pytest_faulthandler", } -FUNCARGNAMES = PytestDeprecationWarning( - "The `funcargnames` attribute was an alias for `fixturenames`, " - "since pytest 2.3 - use the newer attribute instead." -) FILLFUNCARGS = PytestDeprecationWarning( "The `_fillfuncargs` function is deprecated, use " diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index feb145da075..c0fdc587b2a 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -47,7 +47,6 @@ from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS -from _pytest.deprecated import FUNCARGNAMES from _pytest.mark import ParameterSet from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -457,12 +456,6 @@ def fixturenames(self) -> List[str]: result.extend(set(self._fixture_defs).difference(result)) return result - @property - def funcargnames(self) -> List[str]: - """Alias attribute for ``fixturenames`` for pre-2.3 compatibility.""" - warnings.warn(FUNCARGNAMES, stacklevel=2) - return self.fixturenames - @property def node(self): """Underlying collection node (depends on current request scope).""" diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 820b7e86c91..ae5108e762a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -53,7 +53,6 @@ from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH -from _pytest.deprecated import FUNCARGNAMES from _pytest.fixtures import FuncFixtureInfo from _pytest.main import Session from _pytest.mark import MARK_GEN @@ -906,12 +905,6 @@ def __init__( self._calls = [] # type: List[CallSpec2] self._arg2fixturedefs = fixtureinfo.name2fixturedefs - @property - def funcargnames(self) -> List[str]: - """Alias attribute for ``fixturenames`` for pre-2.3 compatibility.""" - warnings.warn(FUNCARGNAMES, stacklevel=2) - return self.fixturenames - def parametrize( self, argnames: Union[str, List[str], Tuple[str, ...]], @@ -1568,12 +1561,6 @@ def _pyfuncitem(self): """(compatonly) for code expecting pytest-2.2 style request objects.""" return self - @property - def funcargnames(self) -> List[str]: - """Alias attribute for ``fixturenames`` for pre-2.3 compatibility.""" - warnings.warn(FUNCARGNAMES, stacklevel=2) - return self.fixturenames - def runtest(self) -> None: """Execute the underlying test function.""" self.ihook.pytest_pyfunc_call(pyfuncitem=self) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8ea1a75ff6f..f007417724a 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -815,28 +815,6 @@ def test_request_fixturenames_dynamic_fixture(self, testdir): result = testdir.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - def test_funcargnames_compatattr(self, testdir): - testdir.makepyfile( - """ - import pytest - def pytest_generate_tests(metafunc): - with pytest.warns(pytest.PytestDeprecationWarning): - assert metafunc.funcargnames == metafunc.fixturenames - @pytest.fixture - def fn(request): - with pytest.warns(pytest.PytestDeprecationWarning): - assert request._pyfuncitem.funcargnames == \ - request._pyfuncitem.fixturenames - with pytest.warns(pytest.PytestDeprecationWarning): - return request.funcargnames, request.fixturenames - - def test_hello(fn): - assert fn[0] == fn[1] - """ - ) - reprec = testdir.inline_run() - reprec.assertoutcome(passed=1) - def test_setupdecorator_and_xunit(self, testdir): testdir.makepyfile( """ From c747dc524878bb476e44d8ee17abd507da0a2c01 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 17 Aug 2020 17:26:06 -0300 Subject: [PATCH 567/823] Drop support for positional arguments in @pytest.fixture --- changelog/5585.breaking.rst | 2 ++ doc/en/deprecations.rst | 7 ++++ src/_pytest/deprecated.py | 5 --- src/_pytest/fixtures.py | 51 +--------------------------- testing/python/fixtures.py | 67 ------------------------------------- 5 files changed, 10 insertions(+), 122 deletions(-) diff --git a/changelog/5585.breaking.rst b/changelog/5585.breaking.rst index 12b9a52dd8b..655c4a42e4a 100644 --- a/changelog/5585.breaking.rst +++ b/changelog/5585.breaking.rst @@ -2,3 +2,5 @@ As per our policy, the following features have been deprecated in the 5.X series removed: * The ``funcargnames`` read-only property of ``FixtureRequest``, ``Metafunc``, and ``Function`` classes. Use ``fixturenames`` attribute. + +* ``@pytest.fixture`` no longer supports positional arguments, pass all arguments by keyword instead. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index d5c7540edff..886bfd7fc40 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -162,6 +162,13 @@ Removed Features As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after an appropriate period of deprecation has passed. +``pytest.fixture`` arguments are keyword only +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 6.0 + +Passing arguments to pytest.fixture() as positional arguments has been removed - pass them by keyword instead. + ``funcargnames`` alias for ``fixturenames`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index d76144d6007..2f3c802510c 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -30,11 +30,6 @@ "See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information." ) -FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning( - "Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them " - "as a keyword argument instead." -) - NODE_USE_FROM_PARENT = UnformattedWarning( PytestDeprecationWarning, "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n" diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index c0fdc587b2a..03d8f5394d3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2,7 +2,6 @@ import inspect import os import sys -import warnings from collections import defaultdict from collections import deque from types import TracebackType @@ -46,7 +45,6 @@ from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config.argparsing import Parser -from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS from _pytest.mark import ParameterSet from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -1246,7 +1244,7 @@ def fixture( # noqa: F811 def fixture( # noqa: F811 fixture_function: Optional[_FixtureFunction] = None, - *args: Any, + *, scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function", params: Optional[Iterable[object]] = None, autouse: bool = False, @@ -1308,53 +1306,6 @@ def fixture( # noqa: F811 name the decorated function ``fixture_`` and then use ``@pytest.fixture(name='')``. """ - # Positional arguments backward compatibility. - # If a kwarg is equal to its default, assume it was not explicitly - # passed, i.e. not duplicated. The more correct way is to use a - # **kwargs and check `in`, but that obfuscates the function signature. - if isinstance(fixture_function, str): - # It's actually the first positional argument, scope. - args = (fixture_function, *args) # type: ignore[unreachable] - fixture_function = None - duplicated_args = [] - if len(args) > 0: - if scope == "function": - scope = args[0] - else: - duplicated_args.append("scope") - if len(args) > 1: - if params is None: - params = args[1] - else: - duplicated_args.append("params") - if len(args) > 2: - if autouse is False: - autouse = args[2] - else: - duplicated_args.append("autouse") - if len(args) > 3: - if ids is None: - ids = args[3] - else: - duplicated_args.append("ids") - if len(args) > 4: - if name is None: - name = args[4] - else: - duplicated_args.append("name") - if len(args) > 5: - raise TypeError( - "fixture() takes 5 positional arguments but {} were given".format(len(args)) - ) - if duplicated_args: - raise TypeError( - "The fixture arguments are defined as positional and keyword: {}. " - "Use only keyword arguments.".format(", ".join(duplicated_args)) - ) - if args: - warnings.warn(FIXTURE_POSITIONAL_ARGUMENTS, stacklevel=2) - # End backward compatiblity. - fixture_marker = FixtureFunctionMarker( scope=scope, params=params, autouse=autouse, ids=ids, name=name, ) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index f007417724a..d54583858ca 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4130,73 +4130,6 @@ def test_fixture_named_request(testdir): ) -def test_fixture_duplicated_arguments() -> None: - """Raise error if there are positional and keyword arguments for the same parameter (#1682).""" - with pytest.raises(TypeError) as excinfo: - - @pytest.fixture("session", scope="session") # type: ignore[call-overload] - def arg(arg): - pass - - assert ( - str(excinfo.value) - == "The fixture arguments are defined as positional and keyword: scope. " - "Use only keyword arguments." - ) - - with pytest.raises(TypeError) as excinfo: - - @pytest.fixture( # type: ignore[call-overload] - "function", - ["p1"], - True, - ["id1"], - "name", - scope="session", - params=["p1"], - autouse=True, - ids=["id1"], - name="name", - ) - def arg2(request): - pass - - assert ( - str(excinfo.value) - == "The fixture arguments are defined as positional and keyword: scope, params, autouse, ids, name. " - "Use only keyword arguments." - ) - - -def test_fixture_with_positionals() -> None: - """Raise warning, but the positionals should still works (#1682).""" - from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS - - with pytest.warns(pytest.PytestDeprecationWarning) as warnings: - - @pytest.fixture("function", [0], True) # type: ignore[call-overload] - def fixture_with_positionals(): - pass - - assert str(warnings[0].message) == str(FIXTURE_POSITIONAL_ARGUMENTS) - - assert fixture_with_positionals._pytestfixturefunction.scope == "function" - assert fixture_with_positionals._pytestfixturefunction.params == (0,) - assert fixture_with_positionals._pytestfixturefunction.autouse - - -def test_fixture_with_too_many_positionals() -> None: - with pytest.raises(TypeError) as excinfo: - - @pytest.fixture("function", [0], True, ["id"], "name", "extra") # type: ignore[call-overload] - def fixture_with_positionals(): - pass - - assert ( - str(excinfo.value) == "fixture() takes 5 positional arguments but 6 were given" - ) - - def test_indirect_fixture_does_not_break_scope(testdir): """Ensure that fixture scope is respected when using indirect fixtures (#570)""" testdir.makepyfile( From 73e06373dc67055bf83d9a1f6cf3242bc90a68c3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 17 Aug 2020 17:34:28 -0300 Subject: [PATCH 568/823] Hard failure when constructing Node subclasses --- changelog/5585.breaking.rst | 2 ++ doc/en/deprecations.rst | 57 ++++++++++++++++++------------------- src/_pytest/deprecated.py | 7 ----- src/_pytest/nodes.py | 10 +++++-- testing/deprecated_test.py | 16 ----------- 5 files changed, 36 insertions(+), 56 deletions(-) diff --git a/changelog/5585.breaking.rst b/changelog/5585.breaking.rst index 655c4a42e4a..bbf133a388d 100644 --- a/changelog/5585.breaking.rst +++ b/changelog/5585.breaking.rst @@ -4,3 +4,5 @@ removed: * The ``funcargnames`` read-only property of ``FixtureRequest``, ``Metafunc``, and ``Function`` classes. Use ``fixturenames`` attribute. * ``@pytest.fixture`` no longer supports positional arguments, pass all arguments by keyword instead. + +* Direct construction of ``Node`` subclasses now raise an error, use ``from_parent`` instead. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 886bfd7fc40..72a0e5b931f 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -43,7 +43,6 @@ it, use `function._request._fillfixtures()` instead, though note this is not a public API and may break in the future. - ``--no-print-logs`` command-line option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -58,35 +57,6 @@ A ``--show-capture`` command-line option was added in ``pytest 3.5.0`` which all display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default). - -Node Construction changed to ``Node.from_parent`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.4 - -The construction of nodes now should use the named constructor ``from_parent``. -This limitation in api surface intends to enable better/simpler refactoring of the collection tree. - -This means that instead of :code:`MyItem(name="foo", parent=collector, obj=42)` -one now has to invoke :code:`MyItem.from_parent(collector, name="foo")`. - -Plugins that wish to support older versions of pytest and suppress the warning can use -`hasattr` to check if `from_parent` exists in that version: - -.. code-block:: python - - def pytest_pycollect_makeitem(collector, name, obj): - if hasattr(MyItem, "from_parent"): - item = MyItem.from_parent(collector, name="foo") - item.obj = 42 - return item - else: - return MyItem(name="foo", parent=collector, obj=42) - -Note that ``from_parent`` should only be called with keyword arguments for the parameters. - - - ``junit_family`` default value change to "xunit2" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -162,6 +132,33 @@ Removed Features As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after an appropriate period of deprecation has passed. +Node Construction changed to ``Node.from_parent`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.0 + +The construction of nodes now should use the named constructor ``from_parent``. +This limitation in api surface intends to enable better/simpler refactoring of the collection tree. + +This means that instead of :code:`MyItem(name="foo", parent=collector, obj=42)` +one now has to invoke :code:`MyItem.from_parent(collector, name="foo")`. + +Plugins that wish to support older versions of pytest and suppress the warning can use +`hasattr` to check if `from_parent` exists in that version: + +.. code-block:: python + + def pytest_pycollect_makeitem(collector, name, obj): + if hasattr(MyItem, "from_parent"): + item = MyItem.from_parent(collector, name="foo") + item.obj = 42 + return item + else: + return MyItem(name="foo", parent=collector, obj=42) + +Note that ``from_parent`` should only be called with keyword arguments for the parameters. + + ``pytest.fixture`` arguments are keyword only ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 2f3c802510c..401cce80d23 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -30,13 +30,6 @@ "See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information." ) -NODE_USE_FROM_PARENT = UnformattedWarning( - PytestDeprecationWarning, - "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n" - "See " - "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent" - " for more details.", -) JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning( "The 'junit_family' default value will change to 'xunit2' in pytest 6.0. See:\n" diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 79e0914438b..8e3e86a508e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -26,7 +26,6 @@ from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH -from _pytest.deprecated import NODE_USE_FROM_PARENT from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError from _pytest.mark.structures import Mark @@ -97,8 +96,13 @@ def ischildnode(baseid: str, nodeid: str) -> bool: class NodeMeta(type): def __call__(self, *k, **kw): - warnings.warn(NODE_USE_FROM_PARENT.format(name=self.__name__), stacklevel=2) - return super().__call__(*k, **kw) + msg = ( + "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n" + "See " + "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent" + " for more details." + ).format(name=self.__name__) + fail(msg, pytrace=False) def _create(self, *k, **kw): return super().__call__(*k, **kw) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 9507d990230..aab7b7b362d 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,11 +1,9 @@ import copy -import inspect import warnings from unittest import mock import pytest from _pytest import deprecated -from _pytest import nodes from _pytest.config import Config from _pytest.pytester import Testdir @@ -106,20 +104,6 @@ def test_foo(): result.stdout.fnmatch_lines([warning_msg]) -def test_node_direct_ctor_warning() -> None: - class MockConfig: - pass - - ms = MockConfig() - with pytest.warns( - DeprecationWarning, - match="Direct construction of .* has been deprecated, please use .*.from_parent.*", - ) as w: - nodes.Node(name="test", config=ms, session=ms, nodeid="None") # type: ignore - assert w[0].lineno == inspect.currentframe().f_lineno - 1 # type: ignore - assert w[0].filename == __file__ - - @pytest.mark.skip(reason="should be reintroduced in 6.1: #7361") def test_fillfuncargs_is_deprecated() -> None: with pytest.warns( From 6ecbd008c417795a8a11563428d1706c4b4230f2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 17 Aug 2020 17:39:13 -0300 Subject: [PATCH 569/823] Change junit_family default to xunit2 --- changelog/5585.breaking.rst | 2 + doc/en/deprecations.rst | 76 ++++++++++++++++++------------------- src/_pytest/deprecated.py | 7 ---- src/_pytest/junitxml.py | 9 ++--- testing/deprecated_test.py | 29 -------------- 5 files changed, 43 insertions(+), 80 deletions(-) diff --git a/changelog/5585.breaking.rst b/changelog/5585.breaking.rst index bbf133a388d..c429324d2bc 100644 --- a/changelog/5585.breaking.rst +++ b/changelog/5585.breaking.rst @@ -6,3 +6,5 @@ removed: * ``@pytest.fixture`` no longer supports positional arguments, pass all arguments by keyword instead. * Direct construction of ``Node`` subclasses now raise an error, use ``from_parent`` instead. + +* The default value for ``junit_family`` has changed to ``xunit2``. If you require the old format, add ``junit_family=xunit1`` to your configuration file. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 72a0e5b931f..df0704d611a 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -57,10 +57,46 @@ A ``--show-capture`` command-line option was added in ``pytest 3.5.0`` which all display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` or ``all`` (the default). + +Result log (``--result-log``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 4.0 + +The ``--result-log`` option produces a stream of test reports which can be +analysed at runtime, but it uses a custom format which requires users to implement their own +parser. + +The `pytest-reportlog `__ plugin provides a ``--report-log`` option, a more standard and extensible alternative, producing +one JSON object per-line, and should cover the same use cases. Please try it out and provide feedback. + +The plan is remove the ``--result-log`` option in pytest 6.0 if ``pytest-reportlog`` proves satisfactory +to all users and is deemed stable. The ``pytest-reportlog`` plugin might even be merged into the core +at some point, depending on the plans for the plugins and number of users using it. + +TerminalReporter.writer +~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.4 + +The ``TerminalReporter.writer`` attribute has been deprecated and should no longer be used. This +was inadvertently exposed as part of the public API of that plugin and ties it too much +with ``py.io.TerminalWriter``. + +Plugins that used ``TerminalReporter.writer`` directly should instead use ``TerminalReporter`` +methods that provide the same functionality. + + +Removed Features +---------------- + +As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after +an appropriate period of deprecation has passed. + ``junit_family`` default value change to "xunit2" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 5.2 +.. versionchanged:: 6.0 The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, which is an update of the old ``xunit1`` format and is supported by default in modern tools @@ -96,46 +132,10 @@ Services known to support the ``xunit2`` format: * `Jenkins `__ with the `JUnit `__ plugin. * `Azure Pipelines `__. - -Result log (``--result-log``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 4.0 - -The ``--result-log`` option produces a stream of test reports which can be -analysed at runtime, but it uses a custom format which requires users to implement their own -parser. - -The `pytest-reportlog `__ plugin provides a ``--report-log`` option, a more standard and extensible alternative, producing -one JSON object per-line, and should cover the same use cases. Please try it out and provide feedback. - -The plan is remove the ``--result-log`` option in pytest 6.0 if ``pytest-reportlog`` proves satisfactory -to all users and is deemed stable. The ``pytest-reportlog`` plugin might even be merged into the core -at some point, depending on the plans for the plugins and number of users using it. - -TerminalReporter.writer -~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.4 - -The ``TerminalReporter.writer`` attribute has been deprecated and should no longer be used. This -was inadvertently exposed as part of the public API of that plugin and ties it too much -with ``py.io.TerminalWriter``. - -Plugins that used ``TerminalReporter.writer`` directly should instead use ``TerminalReporter`` -methods that provide the same functionality. - - -Removed Features ----------------- - -As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after -an appropriate period of deprecation has passed. - Node Construction changed to ``Node.from_parent`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 6.0 +.. versionchanged:: 6.0 The construction of nodes now should use the named constructor ``from_parent``. This limitation in api surface intends to enable better/simpler refactoring of the collection tree. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 401cce80d23..285d7226561 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -30,13 +30,6 @@ "See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information." ) - -JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning( - "The 'junit_family' default value will change to 'xunit2' in pytest 6.0. See:\n" - " https://docs.pytest.org/en/stable/deprecations.html#junit-family-default-value-change-to-xunit2\n" - "for more information." -) - COLLECT_DIRECTORY_HOOK = PytestDeprecationWarning( "The pytest_collect_directory hook is not working.\n" "Please use collect_ignore in conftests or pytest_collection_modifyitems." diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 1e563eb8d25..0acfb49bc93 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -21,7 +21,6 @@ from typing import Union import pytest -from _pytest import deprecated from _pytest import nodes from _pytest import timing from _pytest._code.code import ExceptionRepr @@ -33,7 +32,6 @@ from _pytest.reports import TestReport from _pytest.store import StoreKey from _pytest.terminal import TerminalReporter -from _pytest.warnings import _issue_warning_captured xml_key = StoreKey["LogXML"]() @@ -413,7 +411,9 @@ def pytest_addoption(parser: Parser) -> None: default="total", ) # choices=['total', 'call']) parser.addini( - "junit_family", "Emit XML for schema: one of legacy|xunit1|xunit2", default=None + "junit_family", + "Emit XML for schema: one of legacy|xunit1|xunit2", + default="xunit2", ) @@ -422,9 +422,6 @@ def pytest_configure(config: Config) -> None: # Prevent opening xmllog on worker nodes (xdist). if xmlpath and not hasattr(config, "workerinput"): junit_family = config.getini("junit_family") - if not junit_family: - _issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2) - junit_family = "xunit1" config._store[xml_key] = LogXML( xmlpath, config.option.junitprefix, diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index aab7b7b362d..fb591d7d97c 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -75,35 +75,6 @@ def test_external_plugins_integrated(testdir, plugin): testdir.parseconfig("-p", plugin) -@pytest.mark.parametrize("junit_family", [None, "legacy", "xunit2"]) -def test_warn_about_imminent_junit_family_default_change(testdir, junit_family): - """Show a warning if junit_family is not defined and --junitxml is used (#6179)""" - testdir.makepyfile( - """ - def test_foo(): - pass - """ - ) - if junit_family: - testdir.makeini( - """ - [pytest] - junit_family={junit_family} - """.format( - junit_family=junit_family - ) - ) - - result = testdir.runpytest("--junit-xml=foo.xml") - warning_msg = ( - "*PytestDeprecationWarning: The 'junit_family' default value will change*" - ) - if junit_family: - result.stdout.no_fnmatch_line(warning_msg) - else: - result.stdout.fnmatch_lines([warning_msg]) - - @pytest.mark.skip(reason="should be reintroduced in 6.1: #7361") def test_fillfuncargs_is_deprecated() -> None: with pytest.warns( From 345a59dd53cc07488d2b3939da3240d978e1394e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 17 Aug 2020 17:41:56 -0300 Subject: [PATCH 570/823] Add note about pytest.collect deprecation --- doc/en/deprecations.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index df0704d611a..d5416b93836 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -30,11 +30,19 @@ This hook has an `item` parameter which cannot be serialized by ``pytest-xdist`` Use the ``pytest_warning_recored`` hook instead, which replaces the ``item`` parameter by a ``nodeid`` parameter. +The ``pytest.collect`` module +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.0 + +The ``pytest.collect`` module is no longer part of the public API, all its names +should now be imported from ``pytest`` directly instead. + The ``pytest._fillfuncargs`` function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 5.5 +.. deprecated:: 6.0 This function was kept for backward compatibility with an older plugin. From 457d351941279cd4d93d9fe9a622aa001b9e322e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 17 Aug 2020 17:46:47 -0300 Subject: [PATCH 571/823] Remove deprecated TerminalReporter.writer property --- changelog/5585.breaking.rst | 2 ++ doc/en/deprecations.rst | 16 ++++++++-------- src/_pytest/deprecated.py | 6 ------ src/_pytest/terminal.py | 12 ------------ testing/deprecated_test.py | 31 ------------------------------- 5 files changed, 10 insertions(+), 57 deletions(-) diff --git a/changelog/5585.breaking.rst b/changelog/5585.breaking.rst index c429324d2bc..3f71d3ece7f 100644 --- a/changelog/5585.breaking.rst +++ b/changelog/5585.breaking.rst @@ -8,3 +8,5 @@ removed: * Direct construction of ``Node`` subclasses now raise an error, use ``from_parent`` instead. * The default value for ``junit_family`` has changed to ``xunit2``. If you require the old format, add ``junit_family=xunit1`` to your configuration file. + +* The ``TerminalReporter`` no longer has a ``writer`` attribute. Plugin authors may use the public functions of the ``TerminalReporter`` instead of accessing the ``TerminalWriter`` object directly. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index d5416b93836..fb84647c543 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -82,10 +82,17 @@ The plan is remove the ``--result-log`` option in pytest 6.0 if ``pytest-reportl to all users and is deemed stable. The ``pytest-reportlog`` plugin might even be merged into the core at some point, depending on the plans for the plugins and number of users using it. + +Removed Features +---------------- + +As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after +an appropriate period of deprecation has passed. + TerminalReporter.writer ~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 5.4 +.. versionremoved:: 6.0 The ``TerminalReporter.writer`` attribute has been deprecated and should no longer be used. This was inadvertently exposed as part of the public API of that plugin and ties it too much @@ -94,13 +101,6 @@ with ``py.io.TerminalWriter``. Plugins that used ``TerminalReporter.writer`` directly should instead use ``TerminalReporter`` methods that provide the same functionality. - -Removed Features ----------------- - -As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after -an appropriate period of deprecation has passed. - ``junit_family`` default value change to "xunit2" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 285d7226561..839348282f2 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -42,12 +42,6 @@ ) -TERMINALWRITER_WRITER = PytestDeprecationWarning( - "The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.\n" - "See https://docs.pytest.org/en/stable/deprecations.html#terminalreporter-writer for more information." -) - - MINUS_K_DASH = PytestDeprecationWarning( "The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead." ) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 1d49df4cf89..af68430000c 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -31,7 +31,6 @@ from _pytest import timing from _pytest._code import ExceptionInfo from _pytest._code.code import ExceptionRepr -from _pytest._io import TerminalWriter from _pytest._io.wcwidth import wcswidth from _pytest.compat import order_preserving_dict from _pytest.compat import TYPE_CHECKING @@ -39,7 +38,6 @@ from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser -from _pytest.deprecated import TERMINALWRITER_WRITER from _pytest.nodes import Item from _pytest.nodes import Node from _pytest.reports import BaseReport @@ -335,16 +333,6 @@ def __init__(self, config: Config, file: Optional[TextIO] = None) -> None: self._already_displayed_warnings = None # type: Optional[int] self._keyboardinterrupt_memo = None # type: Optional[ExceptionRepr] - @property - def writer(self) -> TerminalWriter: - warnings.warn(TERMINALWRITER_WRITER, stacklevel=2) - return self._tw - - @writer.setter - def writer(self, value: TerminalWriter) -> None: - warnings.warn(TERMINALWRITER_WRITER, stacklevel=2) - self._tw = value - def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]": """Return whether we should display progress information based on the current config.""" # do not show progress if we are not capturing output (#3038) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index fb591d7d97c..5660b312abf 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,10 +1,8 @@ -import copy import warnings from unittest import mock import pytest from _pytest import deprecated -from _pytest.config import Config from _pytest.pytester import Testdir @@ -36,35 +34,6 @@ def test_pytest_collect_module_deprecated(attribute): getattr(pytest.collect, attribute) -def test_terminal_reporter_writer_attr(pytestconfig: Config) -> None: - """Check that TerminalReporter._tw is also available as 'writer' (#2984) - This attribute has been deprecated in 5.4. - """ - try: - import xdist # noqa - - pytest.skip("xdist workers disable the terminal reporter plugin") - except ImportError: - pass - terminal_reporter = pytestconfig.pluginmanager.get_plugin("terminalreporter") - original_tw = terminal_reporter._tw - - with pytest.warns(pytest.PytestDeprecationWarning) as cw: - assert terminal_reporter.writer is original_tw - assert len(cw) == 1 - assert cw[0].filename == __file__ - - new_tw = copy.copy(original_tw) - with pytest.warns(pytest.PytestDeprecationWarning) as cw: - terminal_reporter.writer = new_tw - try: - assert terminal_reporter._tw is new_tw - finally: - terminal_reporter.writer = original_tw - assert len(cw) == 2 - assert cw[0].filename == cw[1].filename == __file__ - - @pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS)) @pytest.mark.filterwarnings("default") def test_external_plugins_integrated(testdir, plugin): From 52b0cc4f193debc8e16fd386b1644c80d9d329ec Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 17 Aug 2020 17:55:37 -0300 Subject: [PATCH 572/823] Remove broken pytest_collect_directory hook --- doc/en/deprecations.rst | 9 +++++++++ doc/en/reference.rst | 1 - src/_pytest/deprecated.py | 4 ---- src/_pytest/hookspec.py | 11 ----------- src/_pytest/nodes.py | 2 -- testing/acceptance_test.py | 3 +-- testing/test_collection.py | 14 -------------- 7 files changed, 10 insertions(+), 34 deletions(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index fb84647c543..12844265d2b 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -89,6 +89,15 @@ Removed Features As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after an appropriate period of deprecation has passed. + +``pytest_collect_directory`` hook +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 6.0 + +The ``pytest_collect_directory`` has not worked properly for years (it was called +but the results were ignored). Users may consider using :func:`pytest_collection_modifyitems <_pytest.hookspec.pytest_collection_modifyitems>` instead. + TerminalReporter.writer ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 3bc7161aa7b..eb2370ae48d 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -656,7 +656,6 @@ Collection hooks .. autofunction:: pytest_collection .. autofunction:: pytest_ignore_collect -.. autofunction:: pytest_collect_directory .. autofunction:: pytest_collect_file .. autofunction:: pytest_pycollect_makemodule diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 839348282f2..500fbe2f8d9 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -30,10 +30,6 @@ "See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information." ) -COLLECT_DIRECTORY_HOOK = PytestDeprecationWarning( - "The pytest_collect_directory hook is not working.\n" - "Please use collect_ignore in conftests or pytest_collection_modifyitems." -) PYTEST_COLLECT_MODULE = UnformattedWarning( PytestDeprecationWarning, diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index ce435901c44..e60bfe9f9e4 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -12,7 +12,6 @@ import py.path from pluggy import HookspecMarker -from .deprecated import COLLECT_DIRECTORY_HOOK from _pytest.compat import TYPE_CHECKING if TYPE_CHECKING: @@ -262,16 +261,6 @@ def pytest_ignore_collect(path: py.path.local, config: "Config") -> Optional[boo """ -@hookspec(firstresult=True, warn_on_impl=COLLECT_DIRECTORY_HOOK) -def pytest_collect_directory(path: py.path.local, parent) -> Optional[object]: - """Called before traversing a directory for collection files. - - Stops at first non-None result, see :ref:`firstresult`. - - :param py.path.local path: The path to analyze. - """ - - def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]": """Return collection Node or None for the given path. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 8e3e86a508e..addeaf1c21b 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -553,8 +553,6 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: for pat in self._norecursepatterns: if path.check(fnmatch=pat): return False - ihook = self.session.gethookproxy(path) - ihook.pytest_collect_directory(path=path, parent=self) return True def _collectfile( diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index d58b4fd030c..039d8dad969 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -223,13 +223,12 @@ def foo(): "E {}: No module named 'qwerty'".format(exc_name), ] - @pytest.mark.filterwarnings("ignore::pytest.PytestDeprecationWarning") def test_early_skip(self, testdir): testdir.mkdir("xyz") testdir.makeconftest( """ import pytest - def pytest_collect_directory(): + def pytest_collect_file(): pytest.skip("early") """ ) diff --git a/testing/test_collection.py b/testing/test_collection.py index 01dbcc73130..12030e56e49 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -257,20 +257,6 @@ def pytest_collect_file(self, path): assert len(wascalled) == 1 assert wascalled[0].ext == ".abc" - @pytest.mark.filterwarnings("ignore:.*pytest_collect_directory.*") - def test_pytest_collect_directory(self, testdir): - wascalled = [] - - class Plugin: - def pytest_collect_directory(self, path): - wascalled.append(path.basename) - - testdir.mkdir("hello") - testdir.mkdir("world") - pytest.main(testdir.tmpdir, plugins=[Plugin()]) - assert "hello" in wascalled - assert "world" in wascalled - class TestPrunetraceback: def test_custom_repr_failure(self, testdir): From b32c48ee0519f7469077ed9878bbc1d550660d78 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 17 Aug 2020 17:56:16 -0300 Subject: [PATCH 573/823] Add bottom changelog deprecation notice --- changelog/5585.breaking.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog/5585.breaking.rst b/changelog/5585.breaking.rst index 3f71d3ece7f..34e264d0b36 100644 --- a/changelog/5585.breaking.rst +++ b/changelog/5585.breaking.rst @@ -10,3 +10,7 @@ removed: * The default value for ``junit_family`` has changed to ``xunit2``. If you require the old format, add ``junit_family=xunit1`` to your configuration file. * The ``TerminalReporter`` no longer has a ``writer`` attribute. Plugin authors may use the public functions of the ``TerminalReporter`` instead of accessing the ``TerminalWriter`` object directly. + + +For more information consult +`Deprecations and Removals `__ in the docs. From ef946d557cc7b7f030805b94c1dbae51f39fcce4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 17 Aug 2020 18:10:27 -0300 Subject: [PATCH 574/823] Remove resultlog plugin --- changelog/5585.breaking.rst | 2 + doc/en/deprecations.rst | 18 ++- src/_pytest/config/__init__.py | 1 - src/_pytest/deprecated.py | 6 - src/_pytest/resultlog.py | 108 -------------- testing/deprecated_test.py | 20 --- testing/test_conftest.py | 15 -- testing/test_resultlog.py | 256 --------------------------------- testing/test_warnings.py | 25 ---- 9 files changed, 10 insertions(+), 441 deletions(-) delete mode 100644 src/_pytest/resultlog.py delete mode 100644 testing/test_resultlog.py diff --git a/changelog/5585.breaking.rst b/changelog/5585.breaking.rst index 34e264d0b36..0ecba32df81 100644 --- a/changelog/5585.breaking.rst +++ b/changelog/5585.breaking.rst @@ -11,6 +11,8 @@ removed: * The ``TerminalReporter`` no longer has a ``writer`` attribute. Plugin authors may use the public functions of the ``TerminalReporter`` instead of accessing the ``TerminalWriter`` object directly. +* The ``--result-log`` option has been removed. Users are recommended to use the `pytest-reportlog `__ plugin instead. + For more information consult `Deprecations and Removals `__ in the docs. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 12844265d2b..bec321e6963 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -66,10 +66,17 @@ display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` +Removed Features +---------------- + +As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after +an appropriate period of deprecation has passed. + Result log (``--result-log``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 4.0 +.. versionremoved:: 6.0 The ``--result-log`` option produces a stream of test reports which can be analysed at runtime, but it uses a custom format which requires users to implement their own @@ -78,18 +85,9 @@ parser. The `pytest-reportlog `__ plugin provides a ``--report-log`` option, a more standard and extensible alternative, producing one JSON object per-line, and should cover the same use cases. Please try it out and provide feedback. -The plan is remove the ``--result-log`` option in pytest 6.0 if ``pytest-reportlog`` proves satisfactory -to all users and is deemed stable. The ``pytest-reportlog`` plugin might even be merged into the core +The ``pytest-reportlog`` plugin might even be merged into the core at some point, depending on the plans for the plugins and number of users using it. - -Removed Features ----------------- - -As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after -an appropriate period of deprecation has passed. - - ``pytest_collect_directory`` hook ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 453dd834537..1c6ad32882d 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -239,7 +239,6 @@ def directory_arg(path: str, optname: str) -> str: "nose", "assertion", "junitxml", - "resultlog", "doctest", "cacheprovider", "freeze_support", diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 500fbe2f8d9..ecdb60d37f5 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -25,12 +25,6 @@ "function._request._fillfixtures() instead if you cannot avoid reaching into internals." ) -RESULT_LOG = PytestDeprecationWarning( - "--result-log is deprecated, please try the new pytest-reportlog plugin.\n" - "See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information." -) - - PYTEST_COLLECT_MODULE = UnformattedWarning( PytestDeprecationWarning, "pytest.collect.{name} was moved to pytest.{name}\n" diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py deleted file mode 100644 index 686f7f3b0af..00000000000 --- a/src/_pytest/resultlog.py +++ /dev/null @@ -1,108 +0,0 @@ -"""log machine-parseable test session result information to a plain text file.""" -import os -from typing import IO -from typing import Union - -from _pytest._code.code import ExceptionRepr -from _pytest.config import Config -from _pytest.config.argparsing import Parser -from _pytest.reports import CollectReport -from _pytest.reports import TestReport -from _pytest.store import StoreKey - - -resultlog_key = StoreKey["ResultLog"]() - - -def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("terminal reporting", "resultlog plugin options") - group.addoption( - "--resultlog", - "--result-log", - action="store", - metavar="path", - default=None, - help="DEPRECATED path for machine-readable result log.", - ) - - -def pytest_configure(config: Config) -> None: - resultlog = config.option.resultlog - # Prevent opening resultlog on worker nodes (xdist). - if resultlog and not hasattr(config, "workerinput"): - dirname = os.path.dirname(os.path.abspath(resultlog)) - if not os.path.isdir(dirname): - os.makedirs(dirname) - logfile = open(resultlog, "w", 1) # line buffered - config._store[resultlog_key] = ResultLog(config, logfile) - config.pluginmanager.register(config._store[resultlog_key]) - - from _pytest.deprecated import RESULT_LOG - from _pytest.warnings import _issue_warning_captured - - _issue_warning_captured(RESULT_LOG, config.hook, stacklevel=2) - - -def pytest_unconfigure(config: Config) -> None: - resultlog = config._store.get(resultlog_key, None) - if resultlog: - resultlog.logfile.close() - del config._store[resultlog_key] - config.pluginmanager.unregister(resultlog) - - -class ResultLog: - def __init__(self, config: Config, logfile: IO[str]) -> None: - self.config = config - self.logfile = logfile # preferably line buffered - - def write_log_entry(self, testpath: str, lettercode: str, longrepr: str) -> None: - print("{} {}".format(lettercode, testpath), file=self.logfile) - for line in longrepr.splitlines(): - print(" %s" % line, file=self.logfile) - - def log_outcome( - self, report: Union[TestReport, CollectReport], lettercode: str, longrepr: str - ) -> None: - testpath = getattr(report, "nodeid", None) - if testpath is None: - testpath = report.fspath - self.write_log_entry(testpath, lettercode, longrepr) - - def pytest_runtest_logreport(self, report: TestReport) -> None: - if report.when != "call" and report.passed: - return - res = self.config.hook.pytest_report_teststatus( - report=report, config=self.config - ) - code = res[1] # type: str - if code == "x": - longrepr = str(report.longrepr) - elif code == "X": - longrepr = "" - elif report.passed: - longrepr = "" - elif report.skipped: - assert isinstance(report.longrepr, tuple) - longrepr = str(report.longrepr[2]) - else: - longrepr = str(report.longrepr) - self.log_outcome(report, code, longrepr) - - def pytest_collectreport(self, report: CollectReport) -> None: - if not report.passed: - if report.failed: - code = "F" - longrepr = str(report.longrepr) - else: - assert report.skipped - code = "S" - longrepr = "%s:%d: %s" % report.longrepr # type: ignore - self.log_outcome(report, code, longrepr) - - def pytest_internalerror(self, excrepr: ExceptionRepr) -> None: - if excrepr.reprcrash is not None: - path = excrepr.reprcrash.path - else: - path = "cwd:%s" % os.getcwd() - self.write_log_entry(path, "!", str(excrepr)) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 5660b312abf..d3db792ab3c 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -6,26 +6,6 @@ from _pytest.pytester import Testdir -@pytest.mark.filterwarnings("default") -def test_resultlog_is_deprecated(testdir): - result = testdir.runpytest("--help") - result.stdout.fnmatch_lines(["*DEPRECATED path for machine-readable result log*"]) - - testdir.makepyfile( - """ - def test(): - pass - """ - ) - result = testdir.runpytest("--result-log=%s" % testdir.tmpdir.join("result.log")) - result.stdout.fnmatch_lines( - [ - "*--result-log is deprecated, please try the new pytest-reportlog plugin.", - "*See https://docs.pytest.org/en/stable/deprecations.html#result-log-result-log for more information*", - ] - ) - - @pytest.mark.skip(reason="should be reintroduced in 6.1: #7361") @pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore # false positive due to dynamic attribute diff --git a/testing/test_conftest.py b/testing/test_conftest.py index d1a69f4babc..5a476408013 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -306,21 +306,6 @@ def test_no_conftest(testdir): assert result.ret == ExitCode.USAGE_ERROR -def test_conftest_existing_resultlog(testdir): - x = testdir.mkdir("tests") - x.join("conftest.py").write( - textwrap.dedent( - """\ - def pytest_addoption(parser): - parser.addoption("--xyz", action="store_true") - """ - ) - ) - testdir.makefile(ext=".log", result="") # Writes result.log - result = testdir.runpytest("-h", "--resultlog", "result.log") - result.stdout.fnmatch_lines(["*--xyz*"]) - - def test_conftest_existing_junitxml(testdir): x = testdir.mkdir("tests") x.join("conftest.py").write( diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py deleted file mode 100644 index f2eb612c1e7..00000000000 --- a/testing/test_resultlog.py +++ /dev/null @@ -1,256 +0,0 @@ -import os -from io import StringIO -from typing import List - -import _pytest._code -import pytest -from _pytest.pytester import Testdir -from _pytest.resultlog import pytest_configure -from _pytest.resultlog import pytest_unconfigure -from _pytest.resultlog import ResultLog -from _pytest.resultlog import resultlog_key - - -pytestmark = pytest.mark.filterwarnings("ignore:--result-log is deprecated") - - -def test_write_log_entry() -> None: - reslog = ResultLog(None, None) # type: ignore[arg-type] - reslog.logfile = StringIO() - reslog.write_log_entry("name", ".", "") - entry = reslog.logfile.getvalue() - assert entry[-1] == "\n" - entry_lines = entry.splitlines() - assert len(entry_lines) == 1 - assert entry_lines[0] == ". name" - - reslog.logfile = StringIO() - reslog.write_log_entry("name", "s", "Skipped") - entry = reslog.logfile.getvalue() - assert entry[-1] == "\n" - entry_lines = entry.splitlines() - assert len(entry_lines) == 2 - assert entry_lines[0] == "s name" - assert entry_lines[1] == " Skipped" - - reslog.logfile = StringIO() - reslog.write_log_entry("name", "s", "Skipped\n") - entry = reslog.logfile.getvalue() - assert entry[-1] == "\n" - entry_lines = entry.splitlines() - assert len(entry_lines) == 2 - assert entry_lines[0] == "s name" - assert entry_lines[1] == " Skipped" - - reslog.logfile = StringIO() - longrepr = " tb1\n tb 2\nE tb3\nSome Error" - reslog.write_log_entry("name", "F", longrepr) - entry = reslog.logfile.getvalue() - assert entry[-1] == "\n" - entry_lines = entry.splitlines() - assert len(entry_lines) == 5 - assert entry_lines[0] == "F name" - assert entry_lines[1:] == [" " + line for line in longrepr.splitlines()] - - -class TestWithFunctionIntegration: - # XXX (hpk) i think that the resultlog plugin should - # provide a Parser object so that one can remain - # ignorant regarding formatting details. - def getresultlog(self, testdir: Testdir, arg: str) -> List[str]: - resultlog = testdir.tmpdir.join("resultlog") - testdir.plugins.append("resultlog") - args = ["--resultlog=%s" % resultlog] + [arg] - testdir.runpytest(*args) - return [x for x in resultlog.readlines(cr=0) if x] - - def test_collection_report(self, testdir: Testdir) -> None: - ok = testdir.makepyfile(test_collection_ok="") - fail = testdir.makepyfile(test_collection_fail="XXX") - lines = self.getresultlog(testdir, ok) - assert not lines - - lines = self.getresultlog(testdir, fail) - assert lines - assert lines[0].startswith("F ") - assert lines[0].endswith("test_collection_fail.py"), lines[0] - for x in lines[1:]: - assert x.startswith(" ") - assert "XXX" in "".join(lines[1:]) - - def test_log_test_outcomes(self, testdir: Testdir) -> None: - mod = testdir.makepyfile( - test_mod=""" - import pytest - def test_pass(): pass - def test_skip(): pytest.skip("hello") - def test_fail(): raise ValueError("FAIL") - - @pytest.mark.xfail - def test_xfail(): raise ValueError("XFAIL") - @pytest.mark.xfail - def test_xpass(): pass - - """ - ) - lines = self.getresultlog(testdir, mod) - assert len(lines) >= 3 - assert lines[0].startswith(". ") - assert lines[0].endswith("test_pass") - assert lines[1].startswith("s "), lines[1] - assert lines[1].endswith("test_skip") - assert lines[2].find("hello") != -1 - - assert lines[3].startswith("F ") - assert lines[3].endswith("test_fail") - tb = "".join(lines[4:8]) - assert tb.find('raise ValueError("FAIL")') != -1 - - assert lines[8].startswith("x ") - tb = "".join(lines[8:14]) - assert tb.find('raise ValueError("XFAIL")') != -1 - - assert lines[14].startswith("X ") - assert len(lines) == 15 - - @pytest.mark.parametrize("style", ("native", "long", "short")) - def test_internal_exception(self, style) -> None: - # they are produced for example by a teardown failing - # at the end of the run or a failing hook invocation - try: - raise ValueError - except ValueError: - excinfo = _pytest._code.ExceptionInfo.from_current() - file = StringIO() - reslog = ResultLog(None, file) # type: ignore[arg-type] - reslog.pytest_internalerror(excinfo.getrepr(style=style)) - entry = file.getvalue() - entry_lines = entry.splitlines() - - assert entry_lines[0].startswith("! ") - if style != "native": - assert os.path.basename(__file__)[:-9] in entry_lines[0] # .pyc/class - assert entry_lines[-1][0] == " " - assert "ValueError" in entry - - -def test_generic(testdir: Testdir, LineMatcher) -> None: - testdir.plugins.append("resultlog") - testdir.makepyfile( - """ - import pytest - def test_pass(): - pass - def test_fail(): - assert 0 - def test_skip(): - pytest.skip("") - @pytest.mark.xfail - def test_xfail(): - assert 0 - @pytest.mark.xfail(run=False) - def test_xfail_norun(): - assert 0 - """ - ) - testdir.runpytest("--resultlog=result.log") - lines = testdir.tmpdir.join("result.log").readlines(cr=0) - LineMatcher(lines).fnmatch_lines( - [ - ". *:test_pass", - "F *:test_fail", - "s *:test_skip", - "x *:test_xfail", - "x *:test_xfail_norun", - ] - ) - - -def test_makedir_for_resultlog(testdir: Testdir, LineMatcher) -> None: - """--resultlog should automatically create directories for the log file""" - testdir.plugins.append("resultlog") - testdir.makepyfile( - """ - import pytest - def test_pass(): - pass - """ - ) - testdir.runpytest("--resultlog=path/to/result.log") - lines = testdir.tmpdir.join("path/to/result.log").readlines(cr=0) - LineMatcher(lines).fnmatch_lines([". *:test_pass"]) - - -def test_no_resultlog_on_workers(testdir: Testdir) -> None: - config = testdir.parseconfig("-p", "resultlog", "--resultlog=resultlog") - - assert resultlog_key not in config._store - pytest_configure(config) - assert resultlog_key in config._store - pytest_unconfigure(config) - assert resultlog_key not in config._store - - config.workerinput = {} # type: ignore[attr-defined] - pytest_configure(config) - assert resultlog_key not in config._store - pytest_unconfigure(config) - assert resultlog_key not in config._store - - -def test_unknown_teststatus(testdir: Testdir) -> None: - """Ensure resultlog correctly handles unknown status from pytest_report_teststatus - - Inspired on pytest-rerunfailures. - """ - testdir.makepyfile( - """ - def test(): - assert 0 - """ - ) - testdir.makeconftest( - """ - import pytest - - def pytest_report_teststatus(report): - if report.outcome == 'rerun': - return "rerun", "r", "RERUN" - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_makereport(): - res = yield - report = res.get_result() - if report.when == "call": - report.outcome = 'rerun' - """ - ) - result = testdir.runpytest("--resultlog=result.log") - result.stdout.fnmatch_lines( - ["test_unknown_teststatus.py r *[[]100%[]]", "* 1 rerun *"] - ) - - lines = testdir.tmpdir.join("result.log").readlines(cr=0) - assert lines[0] == "r test_unknown_teststatus.py::test" - - -def test_failure_issue380(testdir: Testdir) -> None: - testdir.makeconftest( - """ - import pytest - class MyCollector(pytest.File): - def collect(self): - raise ValueError() - def repr_failure(self, excinfo): - return "somestring" - def pytest_collect_file(path, parent): - return MyCollector(parent=parent, fspath=path) - """ - ) - testdir.makepyfile( - """ - def test_func(): - pass - """ - ) - result = testdir.runpytest("--resultlog=log") - assert result.ret == 2 diff --git a/testing/test_warnings.py b/testing/test_warnings.py index d26c71ca3b2..685e1365df3 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -737,31 +737,6 @@ def test_issue4445_import_plugin(self, testdir, capwarn): assert "config{sep}__init__.py".format(sep=os.sep) in file assert func == "import_plugin" - def test_issue4445_resultlog(self, testdir, capwarn): - """#4445: Make sure the warning points to a reasonable location - See origin of _issue_warning_captured at: _pytest.resultlog.py:35 - """ - testdir.makepyfile( - """ - def test_dummy(): - pass - """ - ) - # Use parseconfigure() because the warning in resultlog.py is triggered in - # the pytest_configure hook - testdir.parseconfigure( - "--result-log={dir}".format(dir=testdir.tmpdir.join("result.log")) - ) - - # with stacklevel=2 the warning originates from resultlog.pytest_configure - # and is thrown when --result-log is used - warning, location = capwarn.captured.pop() - file, _, func = location - - assert "--result-log is deprecated" in str(warning.message) - assert "resultlog.py" in file - assert func == "pytest_configure" - def test_issue4445_issue5928_mark_generator(self, testdir): """#4445 and #5928: Make sure the warning from an unknown mark points to the test file where this mark is used. From 7605150eaab0b6befa1875b9adf6d4e6b168edd2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 17 Aug 2020 18:15:01 -0300 Subject: [PATCH 575/823] Move --no-print-logs removal notice to 'Removed Features' --- doc/en/deprecations.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index bec321e6963..9f15553c740 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -51,6 +51,12 @@ it, use `function._request._fillfixtures()` instead, though note this is not a public API and may break in the future. +Removed Features +---------------- + +As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after +an appropriate period of deprecation has passed. + ``--no-print-logs`` command-line option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -66,12 +72,6 @@ display captured output when tests fail: ``no``, ``stdout``, ``stderr``, ``log`` -Removed Features ----------------- - -As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after -an appropriate period of deprecation has passed. - Result log (``--result-log``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 372a0940053c9fd14280c7feb924fc90f49020d3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 17 Aug 2020 18:21:21 -0300 Subject: [PATCH 576/823] PytestDeprecationWarning no longer a hard error --- src/_pytest/warnings.py | 2 -- testing/test_warnings.py | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 0604aa60b18..4478d8723c6 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -104,8 +104,6 @@ def catch_warnings_for_item( warnings.filterwarnings("always", category=DeprecationWarning) warnings.filterwarnings("always", category=PendingDeprecationWarning) - warnings.filterwarnings("error", category=pytest.PytestDeprecationWarning) - # Filters should have this precedence: mark, cmdline options, ini. # Filters should be applied in the inverse order of precedence. for arg in inifilters: diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 685e1365df3..550ebb4b8e7 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -513,6 +513,9 @@ def test_hidden_by_system(self, testdir, monkeypatch): @pytest.mark.parametrize("change_default", [None, "ini", "cmdline"]) +@pytest.mark.skip( + reason="This test should be enabled again before pytest 7.0 is released" +) def test_deprecation_warning_as_error(testdir, change_default): """This ensures that PytestDeprecationWarnings raised by pytest are turned into errors. From 5e39cd5e71cb624dd5c3bf823a3c9168be3f0ff3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 10:59:55 +0300 Subject: [PATCH 577/823] main: improve message on `pytest path/to/a/directory::mytest` The path part of a `::part1::part2` style collection argument must be a file, not a directory. Previously this crashed with an uncool assert "invalid arg". --- src/_pytest/main.py | 8 ++++++++ testing/test_main.py | 35 +++++++++++++++++------------------ 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 2abfffba0ba..7ff362c341c 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -808,6 +808,7 @@ def resolve_collection_argument( found module. If the path doesn't exist, raise UsageError. + If the path is a directory and selection parts are present, raise UsageError. """ strpath, *parts = str(arg).split("::") if as_pypath: @@ -821,4 +822,11 @@ def resolve_collection_argument( else "file or directory not found: {arg}" ) raise UsageError(msg.format(arg=arg)) + if parts and fspath.is_dir(): + msg = ( + "package argument cannot contain :: selection parts: {arg}" + if as_pypath + else "directory argument cannot contain :: selection parts: {arg}" + ) + raise UsageError(msg.format(arg=arg)) return py.path.local(str(fspath)), parts diff --git a/testing/test_main.py b/testing/test_main.py index 4546c83ba23..71eae16b0e5 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -136,23 +136,21 @@ def test_file(self, root): ["foo", "bar", ""], ) - def test_dir(self, root): + def test_dir(self, root: py.path.local) -> None: """Directory and parts.""" assert resolve_collection_argument(root, "src/pkg") == (root / "src/pkg", []) - assert resolve_collection_argument(root, "src/pkg::") == ( - root / "src/pkg", - [""], - ) - assert resolve_collection_argument(root, "src/pkg::foo::bar") == ( - root / "src/pkg", - ["foo", "bar"], - ) - assert resolve_collection_argument(root, "src/pkg::foo::bar::") == ( - root / "src/pkg", - ["foo", "bar", ""], - ) - def test_pypath(self, root): + with pytest.raises( + UsageError, match=r"directory argument cannot contain :: selection parts" + ): + resolve_collection_argument(root, "src/pkg::") + + with pytest.raises( + UsageError, match=r"directory argument cannot contain :: selection parts" + ): + resolve_collection_argument(root, "src/pkg::foo::bar") + + def test_pypath(self, root: py.path.local) -> None: """Dotted name and parts.""" assert resolve_collection_argument(root, "pkg.test", as_pypath=True) == ( root / "src/pkg/test.py", @@ -165,10 +163,11 @@ def test_pypath(self, root): root / "src/pkg", [], ) - assert resolve_collection_argument(root, "pkg::foo::bar", as_pypath=True) == ( - root / "src/pkg", - ["foo", "bar"], - ) + + with pytest.raises( + UsageError, match=r"package argument cannot contain :: selection parts" + ): + resolve_collection_argument(root, "pkg::foo::bar", as_pypath=True) def test_does_not_exist(self, root): """Given a file/module that does not exist raises UsageError.""" From 75af2bfa06436752165df884d4666402529b1d6a Mon Sep 17 00:00:00 2001 From: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Date: Sat, 22 Aug 2020 16:17:50 +0200 Subject: [PATCH 578/823] Reintroduce warnings postponed in 6.0 (#7637) --- changelog/6981.deprecation.rst | 1 + changelog/7097.deprecation.rst | 6 ++++++ changelog/7210.deprecation.rst | 5 +++++ changelog/7255.deprecation.rst | 1 + src/_pytest/fixtures.py | 5 +++-- src/_pytest/hookspec.py | 5 ++--- src/_pytest/mark/__init__.py | 9 +++++---- src/pytest/collect.py | 6 +++--- testing/deprecated_test.py | 4 ---- 9 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 changelog/6981.deprecation.rst create mode 100644 changelog/7097.deprecation.rst create mode 100644 changelog/7210.deprecation.rst create mode 100644 changelog/7255.deprecation.rst diff --git a/changelog/6981.deprecation.rst b/changelog/6981.deprecation.rst new file mode 100644 index 00000000000..622dd9500ae --- /dev/null +++ b/changelog/6981.deprecation.rst @@ -0,0 +1 @@ +Deprecate the ``pytest.collect`` module: all its names can be imported from ``pytest`` directly. diff --git a/changelog/7097.deprecation.rst b/changelog/7097.deprecation.rst new file mode 100644 index 00000000000..b2aba597b61 --- /dev/null +++ b/changelog/7097.deprecation.rst @@ -0,0 +1,6 @@ +The ``pytest._fillfuncargs`` function is now deprecated. This function was kept +for backward compatibility with an older plugin. + +It's functionality is not meant to be used directly, but if you must replace +it, use `function._request._fillfixtures()` instead, though note this is not +a public API and may break in the future. diff --git a/changelog/7210.deprecation.rst b/changelog/7210.deprecation.rst new file mode 100644 index 00000000000..be0ead2214e --- /dev/null +++ b/changelog/7210.deprecation.rst @@ -0,0 +1,5 @@ +The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'`` +instead. + +The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue +if you use this and want a replacement. diff --git a/changelog/7255.deprecation.rst b/changelog/7255.deprecation.rst new file mode 100644 index 00000000000..c6d56ab5a07 --- /dev/null +++ b/changelog/7255.deprecation.rst @@ -0,0 +1 @@ +The :func:`pytest_warning_captured` hook has been deprecated in favor of :func:`pytest_warning_recorded`, and will be removed in a future version. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 03d8f5394d3..47a7ac2253e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2,6 +2,7 @@ import inspect import os import sys +import warnings from collections import defaultdict from collections import deque from types import TracebackType @@ -45,6 +46,7 @@ from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config.argparsing import Parser +from _pytest.deprecated import FILLFUNCARGS from _pytest.mark import ParameterSet from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -359,8 +361,7 @@ def reorder_items_atscope( def fillfixtures(function: "Function") -> None: """Fill missing funcargs for a test function.""" - # Uncomment this after 6.0 release (#7361) - # warnings.warn(FILLFUNCARGS, stacklevel=2) + warnings.warn(FILLFUNCARGS, stacklevel=2) try: request = function._request except AttributeError: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index e60bfe9f9e4..1906a359871 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -13,6 +13,7 @@ from pluggy import HookspecMarker from _pytest.compat import TYPE_CHECKING +from _pytest.deprecated import WARNING_CAPTURED_HOOK if TYPE_CHECKING: import pdb @@ -723,9 +724,7 @@ def pytest_terminal_summary( """ -# Uncomment this after 6.0 release (#7361) -# @hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK) -@hookspec(historic=True) +@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK) def pytest_warning_captured( warning_message: "warnings.WarningMessage", when: "Literal['config', 'collect', 'runtest']", diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index d677d49c132..6a9b262307e 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -1,5 +1,6 @@ """Generic mechanism for marking and selecting python functions.""" import typing +import warnings from typing import AbstractSet from typing import List from typing import Optional @@ -22,6 +23,8 @@ from _pytest.config import hookimpl from _pytest.config import UsageError from _pytest.config.argparsing import Parser +from _pytest.deprecated import MINUS_K_COLON +from _pytest.deprecated import MINUS_K_DASH from _pytest.store import StoreKey if TYPE_CHECKING: @@ -185,14 +188,12 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None: if keywordexpr.startswith("-"): # To be removed in pytest 7.0.0. - # Uncomment this after 6.0 release (#7361) - # warnings.warn(MINUS_K_DASH, stacklevel=2) + warnings.warn(MINUS_K_DASH, stacklevel=2) keywordexpr = "not " + keywordexpr[1:] selectuntil = False if keywordexpr[-1:] == ":": # To be removed in pytest 7.0.0. - # Uncomment this after 6.0 release (#7361) - # warnings.warn(MINUS_K_COLON, stacklevel=2) + warnings.warn(MINUS_K_COLON, stacklevel=2) selectuntil = True keywordexpr = keywordexpr[:-1] diff --git a/src/pytest/collect.py b/src/pytest/collect.py index 55b4b9b359c..2edf4470f4d 100644 --- a/src/pytest/collect.py +++ b/src/pytest/collect.py @@ -1,10 +1,11 @@ import sys +import warnings from types import ModuleType from typing import Any from typing import List import pytest - +from _pytest.deprecated import PYTEST_COLLECT_MODULE COLLECT_FAKEMODULE_ATTRIBUTES = [ "Collector", @@ -31,8 +32,7 @@ def __dir__(self) -> List[str]: def __getattr__(self, name: str) -> Any: if name not in self.__all__: raise AttributeError(name) - # Uncomment this after 6.0 release (#7361) - # warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2) + warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2) return getattr(pytest, name) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index d3db792ab3c..eb5d527f52b 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -6,7 +6,6 @@ from _pytest.pytester import Testdir -@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361") @pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore # false positive due to dynamic attribute def test_pytest_collect_module_deprecated(attribute): @@ -24,7 +23,6 @@ def test_external_plugins_integrated(testdir, plugin): testdir.parseconfig("-p", plugin) -@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361") def test_fillfuncargs_is_deprecated() -> None: with pytest.warns( pytest.PytestDeprecationWarning, @@ -33,7 +31,6 @@ def test_fillfuncargs_is_deprecated() -> None: pytest._fillfuncargs(mock.Mock()) -@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361") def test_minus_k_dash_is_deprecated(testdir) -> None: threepass = testdir.makepyfile( test_threepass=""" @@ -46,7 +43,6 @@ def test_three(): assert 1 result.stdout.fnmatch_lines(["*The `-k '-expr'` syntax*deprecated*"]) -@pytest.mark.skip(reason="should be reintroduced in 6.1: #7361") def test_minus_k_colon_is_deprecated(testdir) -> None: threepass = testdir.makepyfile( test_threepass=""" From b1354608cca2134e9f791fb900baa2efbba155cf Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 22 Aug 2020 17:23:26 +0300 Subject: [PATCH 579/823] logging: fix handler level restored incorrectly if caplog.set_level is called more than once --- changelog/7672.bugfix.rst | 1 + src/_pytest/logging.py | 3 ++- testing/logging/test_fixture.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog/7672.bugfix.rst diff --git a/changelog/7672.bugfix.rst b/changelog/7672.bugfix.rst new file mode 100644 index 00000000000..88608e1618d --- /dev/null +++ b/changelog/7672.bugfix.rst @@ -0,0 +1 @@ +Fixed log-capturing level restored incorrectly if ``caplog.set_level`` is called more than once. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 5dfd47887a0..95226e8cc3d 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -439,7 +439,8 @@ def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> Non # Save the original log-level to restore it during teardown. self._initial_logger_levels.setdefault(logger, logger_obj.level) logger_obj.setLevel(level) - self._initial_handler_level = self.handler.level + if self._initial_handler_level is None: + self._initial_handler_level = self.handler.level self.handler.setLevel(level) @contextmanager diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index cbd28f798bf..ffd51bcad7a 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -65,6 +65,7 @@ def test_change_level_undos_handler_level(testdir: Testdir) -> None: def test1(caplog): assert caplog.handler.level == 0 + caplog.set_level(9999) caplog.set_level(41) assert caplog.handler.level == 41 From b47b488e3dca00d241daaccf4d155a2440a8d0ae Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 23 Aug 2020 12:32:30 +0300 Subject: [PATCH 580/823] testing: fix flaky test when executed slowly The 0-1 was a bit too optimistic: CI got "no tests ran in 3.98s". --- testing/logging/test_reporting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index bab28aea4ef..7590b576289 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -899,7 +899,7 @@ def test_simple(): expected_lines.extend( [ "*test_collection_collect_only_live_logging.py::test_simple*", - "no tests ran in [0-1].[0-9][0-9]s", + "no tests ran in [0-9].[0-9][0-9]s", ] ) elif verbose == "-qq": From 172b6e15c54bdbfa7de18f5981797c1737bc5f1a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 20 Aug 2020 22:59:15 +0300 Subject: [PATCH 581/823] hookspec: improve collection phase documentation a bit Make it a bit more accurate and use the same format that pytest_runtest_protocol uses. --- src/_pytest/hookspec.py | 35 +++++++++++++++++++++++++---------- src/_pytest/main.py | 14 ++++++++++++++ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index e60bfe9f9e4..2a105191a08 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -205,20 +205,32 @@ def pytest_load_initial_conftests( @hookspec(firstresult=True) def pytest_collection(session: "Session") -> Optional[object]: - """Perform the collection protocol for the given session. + """Perform the collection phase for the given session. Stops at first non-None result, see :ref:`firstresult`. The return value is not used, but only stops further processing. - The hook is meant to set `session.items` to a sequence of items at least, - but normally should follow this procedure: + The default collection phase is this (see individual hooks for full details): - 1. Call the pytest_collectstart hook. - 2. Call the pytest_collectreport hook. - 3. Call the pytest_collection_modifyitems hook. - 4. Call the pytest_collection_finish hook. - 5. Set session.testscollected to the amount of collect items. - 6. Set `session.items` to a list of items. + 1. Starting from ``session`` as the initial collector: + + 1. ``pytest_collectstart(collector)`` + 2. ``report = pytest_make_collect_report(collector)`` + 3. ``pytest_exception_interact(collector, call, report)`` if an interactive exception occurred + 4. For each collected node: + + 1. If an item, ``pytest_itemcollected(item)`` + 2. If a collector, recurse into it. + + 5. ``pytest_collectreport(report)`` + + 2. ``pytest_collection_modifyitems(session, config, items)`` + + 1. ``pytest_deselected(items)`` for any deselected items (may be called multiple times) + + 3. ``pytest_collection_finish(session)`` + 4. Set ``session.items`` to the list of collected items + 5. Set ``session.testscollected`` to the number of collected items You can implement this hook to only perform some action before collection, for example the terminal plugin uses it to start displaying the collection @@ -286,7 +298,10 @@ def pytest_collectreport(report: "CollectReport") -> None: def pytest_deselected(items: Sequence["Item"]) -> None: - """Called for deselected test items, e.g. by keyword.""" + """Called for deselected test items, e.g. by keyword. + + May be called multiple times. + """ @hookspec(firstresult=True) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 2abfffba0ba..d5c198e6cd4 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -538,6 +538,20 @@ def perform_collect( # noqa: F811 def perform_collect( # noqa: F811 self, args: Optional[Sequence[str]] = None, genitems: bool = True ) -> Sequence[Union[nodes.Item, nodes.Collector]]: + """Perform the collection phase for this session. + + This is called by the default + :func:`pytest_collection <_pytest.hookspec.pytest_collection>` hook + implementation; see the documentation of this hook for more details. + For testing purposes, it may also be called directly on a fresh + ``Session``. + + This function normally recursively expands any collectors collected + from the session to their items, and only items are returned. For + testing purposes, this may be suppressed by passing ``genitems=False``, + in which case the return value contains these collectors unexpanded, + and ``session.items`` is empty. + """ hook = self.config.hook try: items = self._perform_collect(args, genitems) From d121d7c917fda743aa4ce4459eb0beac5e3ce3c7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 15 Aug 2020 11:58:44 +0300 Subject: [PATCH 582/823] main: inline Session._perform_collect() into perform_collect() It doesn't add much, mostly just an eye sore, particularly with the overloads. --- src/_pytest/main.py | 86 ++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 48 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 479f34cdcb9..7281ff9d127 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -552,65 +552,55 @@ def perform_collect( # noqa: F811 in which case the return value contains these collectors unexpanded, and ``session.items`` is empty. """ + if args is None: + args = self.config.args + + self.trace("perform_collect", self, args) + self.trace.root.indent += 1 + + self._notfound = [] # type: List[Tuple[str, NoMatch]] + self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]] + self.items = items = [] # type: List[nodes.Item] + hook = self.config.hook + try: - items = self._perform_collect(args, genitems) + initialpaths = [] # type: List[py.path.local] + for arg in args: + fspath, parts = resolve_collection_argument( + self.config.invocation_dir, arg, as_pypath=self.config.option.pyargs + ) + self._initial_parts.append((fspath, parts)) + initialpaths.append(fspath) + self._initialpaths = frozenset(initialpaths) + rep = collect_one_node(self) + self.ihook.pytest_collectreport(report=rep) + self.trace.root.indent -= 1 + if self._notfound: + errors = [] + for arg, exc in self._notfound: + line = "(no name {!r} in any of {!r})".format(arg, exc.args[0]) + errors.append("not found: {}\n{}".format(arg, line)) + raise UsageError(*errors) + if not genitems: + # Type ignored because genitems=False is only used by tests. We don't + # want to change the type of `session.items` for this case. + items = rep.result # type: ignore[assignment] + else: + if rep.passed: + for node in rep.result: + self.items.extend(self.genitems(node)) + self.config.pluginmanager.check_pending() hook.pytest_collection_modifyitems( session=self, config=self.config, items=items ) finally: hook.pytest_collection_finish(session=self) + self.testscollected = len(items) return items - @overload - def _perform_collect( - self, args: Optional[Sequence[str]], genitems: "Literal[True]" - ) -> List[nodes.Item]: - ... - - @overload # noqa: F811 - def _perform_collect( # noqa: F811 - self, args: Optional[Sequence[str]], genitems: bool - ) -> Union[List[Union[nodes.Item]], List[Union[nodes.Item, nodes.Collector]]]: - ... - - def _perform_collect( # noqa: F811 - self, args: Optional[Sequence[str]], genitems: bool - ) -> Union[List[Union[nodes.Item]], List[Union[nodes.Item, nodes.Collector]]]: - if args is None: - args = self.config.args - self.trace("perform_collect", self, args) - self.trace.root.indent += 1 - self._notfound = [] # type: List[Tuple[str, NoMatch]] - initialpaths = [] # type: List[py.path.local] - self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]] - self.items = items = [] # type: List[nodes.Item] - for arg in args: - fspath, parts = resolve_collection_argument( - self.config.invocation_dir, arg, as_pypath=self.config.option.pyargs - ) - self._initial_parts.append((fspath, parts)) - initialpaths.append(fspath) - self._initialpaths = frozenset(initialpaths) - rep = collect_one_node(self) - self.ihook.pytest_collectreport(report=rep) - self.trace.root.indent -= 1 - if self._notfound: - errors = [] - for arg, exc in self._notfound: - line = "(no name {!r} in any of {!r})".format(arg, exc.args[0]) - errors.append("not found: {}\n{}".format(arg, line)) - raise UsageError(*errors) - if not genitems: - return rep.result - else: - if rep.passed: - for node in rep.result: - self.items.extend(self.genitems(node)) - return items - def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: for fspath, parts in self._initial_parts: self.trace("processing argument", (fspath, parts)) From 32edc4655cc3cff79a935fbd4e9f474fa3001ffd Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 15 Aug 2020 12:38:57 +0300 Subject: [PATCH 583/823] main: inline Session._matchnodes() into Session.matchnodes() Similar to the previous commit, this makes things more straightforward. --- src/_pytest/main.py | 85 ++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 7281ff9d127..06619023e99 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -709,52 +709,51 @@ def matchnodes( ) -> Sequence[Union[nodes.Item, nodes.Collector]]: self.trace("matchnodes", matching, names) self.trace.root.indent += 1 - nodes = self._matchnodes(matching, names) - num = len(nodes) - self.trace("matchnodes finished -> ", num, "nodes") - self.trace.root.indent -= 1 - if num == 0: - raise NoMatch(matching, names[:1]) - return nodes - def _matchnodes( - self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str], - ) -> Sequence[Union[nodes.Item, nodes.Collector]]: if not matching or not names: - return matching - name = names[0] - assert name - nextnames = names[1:] - resultnodes = [] # type: List[Union[nodes.Item, nodes.Collector]] - for node in matching: - if isinstance(node, nodes.Item): - if not names: - resultnodes.append(node) - continue - assert isinstance(node, nodes.Collector) - key = (type(node), node.nodeid) - if key in self._collection_node_cache3: - rep = self._collection_node_cache3[key] - else: - rep = collect_one_node(node) - self._collection_node_cache3[key] = rep - if rep.passed: - has_matched = False - for x in rep.result: - # TODO: Remove parametrized workaround once collection structure contains parametrization. - if x.name == name or x.name.split("[")[0] == name: + result = matching + else: + name = names[0] + assert name + nextnames = names[1:] + resultnodes = [] # type: List[Union[nodes.Item, nodes.Collector]] + for node in matching: + if isinstance(node, nodes.Item): + if not names: + resultnodes.append(node) + continue + assert isinstance(node, nodes.Collector) + key = (type(node), node.nodeid) + if key in self._collection_node_cache3: + rep = self._collection_node_cache3[key] + else: + rep = collect_one_node(node) + self._collection_node_cache3[key] = rep + if rep.passed: + has_matched = False + for x in rep.result: + # TODO: Remove parametrized workaround once collection structure contains parametrization. + if x.name == name or x.name.split("[")[0] == name: + resultnodes.extend(self.matchnodes([x], nextnames)) + has_matched = True + # XXX Accept IDs that don't have "()" for class instances. + if not has_matched and len(rep.result) == 1 and x.name == "()": + nextnames.insert(0, name) resultnodes.extend(self.matchnodes([x], nextnames)) - has_matched = True - # XXX Accept IDs that don't have "()" for class instances. - if not has_matched and len(rep.result) == 1 and x.name == "()": - nextnames.insert(0, name) - resultnodes.extend(self.matchnodes([x], nextnames)) - else: - # Report collection failures here to avoid failing to run some test - # specified in the command line because the module could not be - # imported (#134). - node.ihook.pytest_collectreport(report=rep) - return resultnodes + else: + # Report collection failures here to avoid failing to run some test + # specified in the command line because the module could not be + # imported (#134). + node.ihook.pytest_collectreport(report=rep) + result = resultnodes + + self.trace("matchnodes finished -> ", len(result), "nodes") + self.trace.root.indent -= 1 + + if not result: + raise NoMatch(matching, names[:1]) + else: + return result def genitems( self, node: Union[nodes.Item, nodes.Collector] From 4b8e1a17718ae1459604cea128c72d46663ad9bd Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 14 Aug 2020 14:05:00 +0300 Subject: [PATCH 584/823] Revert "Move common code between Session and Package to FSCollector" This reverts commit f10ab021e21a44e2f0fa2be66660c4a6d4b7a61a. The commit was good in that it removed a non-trivial amount of code duplication. However it was done in the wrong layer (nodes.py) and split up a major part of the collection (the filesystem traversal) to a separate class making it harder to understand. We should try to reduce the duplication, but in a more appropriate manner. --- src/_pytest/main.py | 37 +++++++++++++++++++++++++++++++++++++ src/_pytest/nodes.py | 39 --------------------------------------- src/_pytest/python.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 39 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 06619023e99..f71ef86deee 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -523,6 +523,43 @@ def gethookproxy(self, fspath: py.path.local): proxy = self.config.hook return proxy + def _recurse(self, direntry: "os.DirEntry[str]") -> bool: + if direntry.name == "__pycache__": + return False + path = py.path.local(direntry.path) + ihook = self.gethookproxy(path.dirpath()) + if ihook.pytest_ignore_collect(path=path, config=self.config): + return False + norecursepatterns = self.config.getini("norecursedirs") + for pat in norecursepatterns: + if path.check(fnmatch=pat): + return False + return True + + def _collectfile( + self, path: py.path.local, handle_dupes: bool = True + ) -> Sequence[nodes.Collector]: + assert ( + path.isfile() + ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( + path, path.isdir(), path.exists(), path.islink() + ) + ihook = self.gethookproxy(path) + if not self.isinitpath(path): + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () + + if handle_dupes: + keepduplicates = self.config.getoption("keepduplicates") + if not keepduplicates: + duplicate_paths = self.config.pluginmanager._duplicatepaths + if path in duplicate_paths: + return () + else: + duplicate_paths.add(path) + + return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] + @overload def perform_collect( self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ... diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index addeaf1c21b..26fab67fe68 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -8,7 +8,6 @@ from typing import Iterator from typing import List from typing import Optional -from typing import Sequence from typing import Set from typing import Tuple from typing import TypeVar @@ -528,8 +527,6 @@ def __init__( super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath) - self._norecursepatterns = self.config.getini("norecursedirs") - @classmethod def from_parent(cls, parent, *, fspath, **kw): """The public constructor.""" @@ -543,42 +540,6 @@ def isinitpath(self, path: py.path.local) -> bool: warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.isinitpath(path) - def _recurse(self, direntry: "os.DirEntry[str]") -> bool: - if direntry.name == "__pycache__": - return False - path = py.path.local(direntry.path) - ihook = self.session.gethookproxy(path.dirpath()) - if ihook.pytest_ignore_collect(path=path, config=self.config): - return False - for pat in self._norecursepatterns: - if path.check(fnmatch=pat): - return False - return True - - def _collectfile( - self, path: py.path.local, handle_dupes: bool = True - ) -> Sequence[Collector]: - assert ( - path.isfile() - ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( - path, path.isdir(), path.exists(), path.islink() - ) - ihook = self.session.gethookproxy(path) - if not self.session.isinitpath(path): - if ihook.pytest_ignore_collect(path=path, config=self.config): - return () - - if handle_dupes: - keepduplicates = self.config.getoption("keepduplicates") - if not keepduplicates: - duplicate_paths = self.config.pluginmanager._duplicatepaths - if path in duplicate_paths: - return () - else: - duplicate_paths.add(path) - - return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] - class File(FSCollector): """Base class for collecting tests from a file.""" diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ae5108e762a..63fa539d207 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -634,6 +634,43 @@ def isinitpath(self, path: py.path.local) -> bool: warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.isinitpath(path) + def _recurse(self, direntry: "os.DirEntry[str]") -> bool: + if direntry.name == "__pycache__": + return False + path = py.path.local(direntry.path) + ihook = self.session.gethookproxy(path.dirpath()) + if ihook.pytest_ignore_collect(path=path, config=self.config): + return False + norecursepatterns = self.config.getini("norecursedirs") + for pat in norecursepatterns: + if path.check(fnmatch=pat): + return False + return True + + def _collectfile( + self, path: py.path.local, handle_dupes: bool = True + ) -> typing.Sequence[nodes.Collector]: + assert ( + path.isfile() + ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( + path, path.isdir(), path.exists(), path.islink() + ) + ihook = self.session.gethookproxy(path) + if not self.session.isinitpath(path): + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () + + if handle_dupes: + keepduplicates = self.config.getoption("keepduplicates") + if not keepduplicates: + duplicate_paths = self.config.pluginmanager._duplicatepaths + if path in duplicate_paths: + return () + else: + duplicate_paths.add(path) + + return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: this_path = self.fspath.dirpath() init_module = this_path.join("__init__.py") From 57aca11d4a468cf9a40b93dcb3bdd0f253ead017 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 20 Aug 2020 23:51:08 +0300 Subject: [PATCH 585/823] hookspec: type annotate parent argument to pytest_collect_file --- src/_pytest/doctest.py | 3 ++- src/_pytest/hookspec.py | 8 +++++--- src/_pytest/python.py | 4 +++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index acedd389b32..c744bb369ea 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -32,6 +32,7 @@ from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureRequest +from _pytest.nodes import Collector from _pytest.outcomes import OutcomeException from _pytest.pathlib import import_path from _pytest.python_api import approx @@ -118,7 +119,7 @@ def pytest_unconfigure() -> None: def pytest_collect_file( - path: py.path.local, parent + path: py.path.local, parent: Collector, ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: config = parent.config if path.ext == ".py": diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 8e6be7d56d2..d4aaba1ec23 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -274,10 +274,12 @@ def pytest_ignore_collect(path: py.path.local, config: "Config") -> Optional[boo """ -def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]": - """Return collection Node or None for the given path. +def pytest_collect_file( + path: py.path.local, parent: "Collector" +) -> "Optional[Collector]": + """Create a Collector for the given path, or None if not relevant. - Any new node needs to have the specified ``parent`` as a parent. + The new node needs to have the specified ``parent`` as a parent. :param py.path.local path: The path to collect. """ diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 63fa539d207..7aa5b422275 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -184,7 +184,9 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: return True -def pytest_collect_file(path: py.path.local, parent) -> Optional["Module"]: +def pytest_collect_file( + path: py.path.local, parent: nodes.Collector +) -> Optional["Module"]: ext = path.ext if ext == ".py": if not parent.session.isinitpath(path): From 0b41b79dcb985f2fbb56772ddedcfc6f0e210748 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 10:47:29 +0300 Subject: [PATCH 586/823] main: better solution to a type ignore --- src/_pytest/main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index f71ef86deee..f31defe676c 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -597,10 +597,11 @@ def perform_collect( # noqa: F811 self._notfound = [] # type: List[Tuple[str, NoMatch]] self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]] - self.items = items = [] # type: List[nodes.Item] + self.items = [] # type: List[nodes.Item] hook = self.config.hook + items = self.items # type: Sequence[Union[nodes.Item, nodes.Collector]] try: initialpaths = [] # type: List[py.path.local] for arg in args: @@ -620,9 +621,7 @@ def perform_collect( # noqa: F811 errors.append("not found: {}\n{}".format(arg, line)) raise UsageError(*errors) if not genitems: - # Type ignored because genitems=False is only used by tests. We don't - # want to change the type of `session.items` for this case. - items = rep.result # type: ignore[assignment] + items = rep.result else: if rep.passed: for node in rep.result: From 5356a0979a4b18a111cf5ad4c53484864369652e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 13:39:46 +0300 Subject: [PATCH 587/823] main: small code simplification in matchnodes --- src/_pytest/main.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index f31defe676c..d214f0c2962 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -656,7 +656,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: self._collection_pkg_roots.clear() def _collect( - self, argpath: py.path.local, names: List[str] + self, argpath: py.path.local, names: Sequence[str] ) -> Iterator[Union[nodes.Item, nodes.Collector]]: from _pytest.python import Package @@ -741,7 +741,9 @@ def _collect( yield from m def matchnodes( - self, matching: Sequence[Union[nodes.Item, nodes.Collector]], names: List[str], + self, + matching: Sequence[Union[nodes.Item, nodes.Collector]], + names: Sequence[str], ) -> Sequence[Union[nodes.Item, nodes.Collector]]: self.trace("matchnodes", matching, names) self.trace.root.indent += 1 @@ -751,7 +753,6 @@ def matchnodes( else: name = names[0] assert name - nextnames = names[1:] resultnodes = [] # type: List[Union[nodes.Item, nodes.Collector]] for node in matching: if isinstance(node, nodes.Item): @@ -770,12 +771,11 @@ def matchnodes( for x in rep.result: # TODO: Remove parametrized workaround once collection structure contains parametrization. if x.name == name or x.name.split("[")[0] == name: - resultnodes.extend(self.matchnodes([x], nextnames)) + resultnodes.extend(self.matchnodes([x], names[1:])) has_matched = True # XXX Accept IDs that don't have "()" for class instances. if not has_matched and len(rep.result) == 1 and x.name == "()": - nextnames.insert(0, name) - resultnodes.extend(self.matchnodes([x], nextnames)) + resultnodes.extend(self.matchnodes([x], names)) else: # Report collection failures here to avoid failing to run some test # specified in the command line because the module could not be From 1b2de81404172d8b48d93e192b59e4c397e08e8d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 13:50:13 +0300 Subject: [PATCH 588/823] main: remove unneeded condition in matchnodes The end result in the `else` branch is the same, but flows naturally. --- src/_pytest/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d214f0c2962..71c23007748 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -748,7 +748,7 @@ def matchnodes( self.trace("matchnodes", matching, names) self.trace.root.indent += 1 - if not matching or not names: + if not names: result = matching else: name = names[0] From adaec2da90c006e4c3317e49b8cbb3f5b6a8dc72 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 13:53:05 +0300 Subject: [PATCH 589/823] main: remove impossible condition in matchnodes Already covered in a condition above. --- src/_pytest/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 71c23007748..602f5fbd283 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -756,8 +756,6 @@ def matchnodes( resultnodes = [] # type: List[Union[nodes.Item, nodes.Collector]] for node in matching: if isinstance(node, nodes.Item): - if not names: - resultnodes.append(node) continue assert isinstance(node, nodes.Collector) key = (type(node), node.nodeid) From a2c919d350e5f52287a42187853ed0e090168fe1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 14:19:14 +0300 Subject: [PATCH 590/823] main: refactor a bit to reduce indentation --- src/_pytest/main.py | 60 +++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 602f5fbd283..1f7340720f4 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -748,38 +748,34 @@ def matchnodes( self.trace("matchnodes", matching, names) self.trace.root.indent += 1 - if not names: - result = matching - else: - name = names[0] - assert name - resultnodes = [] # type: List[Union[nodes.Item, nodes.Collector]] - for node in matching: - if isinstance(node, nodes.Item): - continue - assert isinstance(node, nodes.Collector) - key = (type(node), node.nodeid) - if key in self._collection_node_cache3: - rep = self._collection_node_cache3[key] - else: - rep = collect_one_node(node) - self._collection_node_cache3[key] = rep - if rep.passed: - has_matched = False - for x in rep.result: - # TODO: Remove parametrized workaround once collection structure contains parametrization. - if x.name == name or x.name.split("[")[0] == name: - resultnodes.extend(self.matchnodes([x], names[1:])) - has_matched = True - # XXX Accept IDs that don't have "()" for class instances. - if not has_matched and len(rep.result) == 1 and x.name == "()": - resultnodes.extend(self.matchnodes([x], names)) - else: - # Report collection failures here to avoid failing to run some test - # specified in the command line because the module could not be - # imported (#134). - node.ihook.pytest_collectreport(report=rep) - result = resultnodes + result = [] + for node in matching: + if not names: + result.append(node) + continue + if not isinstance(node, nodes.Collector): + continue + key = (type(node), node.nodeid) + if key in self._collection_node_cache3: + rep = self._collection_node_cache3[key] + else: + rep = collect_one_node(node) + self._collection_node_cache3[key] = rep + if rep.passed: + has_matched = False + for x in rep.result: + # TODO: Remove parametrized workaround once collection structure contains parametrization. + if x.name == names[0] or x.name.split("[")[0] == names[0]: + result.extend(self.matchnodes([x], names[1:])) + has_matched = True + # XXX Accept IDs that don't have "()" for class instances. + if not has_matched and len(rep.result) == 1 and x.name == "()": + result.extend(self.matchnodes([x], names)) + else: + # Report collection failures here to avoid failing to run some test + # specified in the command line because the module could not be + # imported (#134). + node.ihook.pytest_collectreport(report=rep) self.trace("matchnodes finished -> ", len(result), "nodes") self.trace.root.indent -= 1 From 0c6b2f39b2fa4532813b5995038af6d143405c88 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 14:45:03 +0300 Subject: [PATCH 591/823] main: move NoMatch raising to _collect() This is a more sensible interface for matchnodes. This also fixes a sort-of bug where a recursive call to matchnodes raises NoMatch which would terminate the entire tree, even if other branches may find a match. Though I don't think it's actually possible. --- src/_pytest/main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 1f7340720f4..8d898426c5b 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -724,6 +724,8 @@ def _collect( if col: self._collection_node_cache1[argpath] = col m = self.matchnodes(col, names) + if not m: + raise NoMatch(col) # If __init__.py was the only file requested, then the matched node will be # the corresponding Package, and the first yielded item will be the __init__ # Module itself, so just use that. If this special case isn't taken, then all @@ -780,10 +782,7 @@ def matchnodes( self.trace("matchnodes finished -> ", len(result), "nodes") self.trace.root.indent -= 1 - if not result: - raise NoMatch(matching, names[:1]) - else: - return result + return result def genitems( self, node: Union[nodes.Item, nodes.Collector] From 841521fedb9e04a67479b286773a38f0fc4c1c90 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 15:05:54 +0300 Subject: [PATCH 592/823] main: only perform one recursive matchnodes call per node --- src/_pytest/main.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 8d898426c5b..e47752f911a 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -764,15 +764,16 @@ def matchnodes( rep = collect_one_node(node) self._collection_node_cache3[key] = rep if rep.passed: - has_matched = False + submatching = [] for x in rep.result: # TODO: Remove parametrized workaround once collection structure contains parametrization. if x.name == names[0] or x.name.split("[")[0] == names[0]: - result.extend(self.matchnodes([x], names[1:])) - has_matched = True + submatching.append(x) + if submatching: + result.extend(self.matchnodes(submatching, names[1:])) # XXX Accept IDs that don't have "()" for class instances. - if not has_matched and len(rep.result) == 1 and x.name == "()": - result.extend(self.matchnodes([x], names)) + elif len(rep.result) == 1 and rep.result[0].name == "()": + result.extend(self.matchnodes(rep.result, names)) else: # Report collection failures here to avoid failing to run some test # specified in the command line because the module could not be From c2256189ae2bd1cd0b5e2c34b5861b6f9255c028 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 16:50:28 +0300 Subject: [PATCH 593/823] main: make matchnodes non-recursive It's a little more sane this way. --- src/_pytest/main.py | 68 +++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index e47752f911a..7b527266636 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -747,42 +747,44 @@ def matchnodes( matching: Sequence[Union[nodes.Item, nodes.Collector]], names: Sequence[str], ) -> Sequence[Union[nodes.Item, nodes.Collector]]: - self.trace("matchnodes", matching, names) - self.trace.root.indent += 1 - result = [] - for node in matching: - if not names: - result.append(node) - continue - if not isinstance(node, nodes.Collector): - continue - key = (type(node), node.nodeid) - if key in self._collection_node_cache3: - rep = self._collection_node_cache3[key] - else: - rep = collect_one_node(node) - self._collection_node_cache3[key] = rep - if rep.passed: - submatching = [] - for x in rep.result: - # TODO: Remove parametrized workaround once collection structure contains parametrization. - if x.name == names[0] or x.name.split("[")[0] == names[0]: - submatching.append(x) - if submatching: - result.extend(self.matchnodes(submatching, names[1:])) - # XXX Accept IDs that don't have "()" for class instances. - elif len(rep.result) == 1 and rep.result[0].name == "()": - result.extend(self.matchnodes(rep.result, names)) - else: - # Report collection failures here to avoid failing to run some test - # specified in the command line because the module could not be - # imported (#134). - node.ihook.pytest_collectreport(report=rep) + work = [(matching, names)] + while work: + self.trace("matchnodes", matching, names) + self.trace.root.indent += 1 - self.trace("matchnodes finished -> ", len(result), "nodes") - self.trace.root.indent -= 1 + matching, names = work.pop() + for node in matching: + if not names: + result.append(node) + continue + if not isinstance(node, nodes.Collector): + continue + key = (type(node), node.nodeid) + if key in self._collection_node_cache3: + rep = self._collection_node_cache3[key] + else: + rep = collect_one_node(node) + self._collection_node_cache3[key] = rep + if rep.passed: + submatching = [] + for x in rep.result: + # TODO: Remove parametrized workaround once collection structure contains parametrization. + if x.name == names[0] or x.name.split("[")[0] == names[0]: + submatching.append(x) + if submatching: + work.append((submatching, names[1:])) + # XXX Accept IDs that don't have "()" for class instances. + elif len(rep.result) == 1 and rep.result[0].name == "()": + work.append((rep.result, names)) + else: + # Report collection failures here to avoid failing to run some test + # specified in the command line because the module could not be + # imported (#134). + node.ihook.pytest_collectreport(report=rep) + self.trace("matchnodes finished -> ", len(result), "nodes") + self.trace.root.indent -= 1 return result def genitems( From c4fd4616176a685f25ba79249cd78574b4a6fba2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 17:03:52 +0300 Subject: [PATCH 594/823] main: better name for _collection_node_cache3 The weird name was due to f3967333a145d8a793a0ab53ac5e0cb0b6c87cac, now that I understand it a bit better can give it a more descriptive name. --- src/_pytest/main.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 7b527266636..ec069808bb7 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -454,7 +454,10 @@ def __init__(self, config: Config) -> None: self._collection_node_cache2 = ( {} ) # type: Dict[Tuple[Type[nodes.Collector], py.path.local], nodes.Collector] - self._collection_node_cache3 = ( + + # Keep track of any collected collectors in matchnodes paths, so they + # are not collected more than once. + self._collection_matchnodes_cache = ( {} ) # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport] @@ -652,7 +655,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: self.trace.root.indent -= 1 self._collection_node_cache1.clear() self._collection_node_cache2.clear() - self._collection_node_cache3.clear() + self._collection_matchnodes_cache.clear() self._collection_pkg_roots.clear() def _collect( @@ -677,7 +680,6 @@ def _collect( if col: if isinstance(col[0], Package): self._collection_pkg_roots[str(parent)] = col[0] - # Always store a list in the cache, matchnodes expects it. self._collection_node_cache1[col[0].fspath] = [col[0]] # If it's a directory argument, recurse and look for any Subpackages. @@ -761,11 +763,11 @@ def matchnodes( if not isinstance(node, nodes.Collector): continue key = (type(node), node.nodeid) - if key in self._collection_node_cache3: - rep = self._collection_node_cache3[key] + if key in self._collection_matchnodes_cache: + rep = self._collection_matchnodes_cache[key] else: rep = collect_one_node(node) - self._collection_node_cache3[key] = rep + self._collection_matchnodes_cache[key] = rep if rep.passed: submatching = [] for x in rep.result: From eec13ba57ed812fe094b2ba12b57cabd08739a34 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 19:29:13 +0300 Subject: [PATCH 595/823] main: get rid of NoMatch Things are easier to understand without the weird exception. --- src/_pytest/main.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index ec069808bb7..274266faf56 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -402,10 +402,6 @@ def __getattr__(self, name: str): return x -class NoMatch(Exception): - """Matching cannot locate matching names.""" - - class Interrupted(KeyboardInterrupt): """Signals that the test run was interrupted.""" @@ -598,7 +594,7 @@ def perform_collect( # noqa: F811 self.trace("perform_collect", self, args) self.trace.root.indent += 1 - self._notfound = [] # type: List[Tuple[str, NoMatch]] + self._notfound = [] # type: List[Tuple[str, Sequence[nodes.Collector]]] self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]] self.items = [] # type: List[nodes.Item] @@ -619,8 +615,8 @@ def perform_collect( # noqa: F811 self.trace.root.indent -= 1 if self._notfound: errors = [] - for arg, exc in self._notfound: - line = "(no name {!r} in any of {!r})".format(arg, exc.args[0]) + for arg, cols in self._notfound: + line = "(no name {!r} in any of {!r})".format(arg, cols) errors.append("not found: {}\n{}".format(arg, line)) raise UsageError(*errors) if not genitems: @@ -644,14 +640,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: for fspath, parts in self._initial_parts: self.trace("processing argument", (fspath, parts)) self.trace.root.indent += 1 - try: - yield from self._collect(fspath, parts) - except NoMatch as exc: - report_arg = "::".join((str(fspath), *parts)) - # we are inside a make_report hook so - # we cannot directly pass through the exception - self._notfound.append((report_arg, exc)) - + yield from self._collect(fspath, parts) self.trace.root.indent -= 1 self._collection_node_cache1.clear() self._collection_node_cache2.clear() @@ -727,7 +716,10 @@ def _collect( self._collection_node_cache1[argpath] = col m = self.matchnodes(col, names) if not m: - raise NoMatch(col) + report_arg = "::".join((str(argpath), *names)) + self._notfound.append((report_arg, col)) + return + # If __init__.py was the only file requested, then the matched node will be # the corresponding Package, and the first yielded item will be the __init__ # Module itself, so just use that. If this special case isn't taken, then all From d0e8b71404efc19554dbe5b38a226c31b7e281b9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 22 Aug 2020 11:15:10 +0300 Subject: [PATCH 596/823] main: inline _collect() into collect() This removes an unhelpful level of indirection and enables some upcoming upcoming simplifications. --- src/_pytest/main.py | 181 ++++++++++++++++++++++---------------------- 1 file changed, 90 insertions(+), 91 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 274266faf56..a581cbe23dc 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -637,104 +637,103 @@ def perform_collect( # noqa: F811 return items def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: - for fspath, parts in self._initial_parts: - self.trace("processing argument", (fspath, parts)) - self.trace.root.indent += 1 - yield from self._collect(fspath, parts) - self.trace.root.indent -= 1 - self._collection_node_cache1.clear() - self._collection_node_cache2.clear() - self._collection_matchnodes_cache.clear() - self._collection_pkg_roots.clear() - - def _collect( - self, argpath: py.path.local, names: Sequence[str] - ) -> Iterator[Union[nodes.Item, nodes.Collector]]: from _pytest.python import Package - # Start with a Session root, and delve to argpath item (dir or file) - # and stack all Packages found on the way. - # No point in finding packages when collecting doctests. - if not self.config.getoption("doctestmodules", False): - pm = self.config.pluginmanager - for parent in reversed(argpath.parts()): - if pm._confcutdir and pm._confcutdir.relto(parent): - break - - if parent.isdir(): - pkginit = parent.join("__init__.py") - if pkginit.isfile(): - if pkginit not in self._collection_node_cache1: - col = self._collectfile(pkginit, handle_dupes=False) - if col: - if isinstance(col[0], Package): - self._collection_pkg_roots[str(parent)] = col[0] - self._collection_node_cache1[col[0].fspath] = [col[0]] - - # If it's a directory argument, recurse and look for any Subpackages. - # Let the Package collector deal with subnodes, don't collect here. - if argpath.check(dir=1): - assert not names, "invalid arg {!r}".format((argpath, names)) - - seen_dirs = set() # type: Set[py.path.local] - for direntry in visit(str(argpath), self._recurse): - if not direntry.is_file(): - continue - - path = py.path.local(direntry.path) - dirpath = path.dirpath() + for argpath, names in self._initial_parts: + self.trace("processing argument", (argpath, names)) + self.trace.root.indent += 1 - if dirpath not in seen_dirs: - # Collect packages first. - seen_dirs.add(dirpath) - pkginit = dirpath.join("__init__.py") - if pkginit.exists(): - for x in self._collectfile(pkginit): + # Start with a Session root, and delve to argpath item (dir or file) + # and stack all Packages found on the way. + # No point in finding packages when collecting doctests. + if not self.config.getoption("doctestmodules", False): + pm = self.config.pluginmanager + for parent in reversed(argpath.parts()): + if pm._confcutdir and pm._confcutdir.relto(parent): + break + + if parent.isdir(): + pkginit = parent.join("__init__.py") + if pkginit.isfile(): + if pkginit not in self._collection_node_cache1: + col = self._collectfile(pkginit, handle_dupes=False) + if col: + if isinstance(col[0], Package): + self._collection_pkg_roots[str(parent)] = col[0] + self._collection_node_cache1[col[0].fspath] = [ + col[0] + ] + + # If it's a directory argument, recurse and look for any Subpackages. + # Let the Package collector deal with subnodes, don't collect here. + if argpath.check(dir=1): + assert not names, "invalid arg {!r}".format((argpath, names)) + + seen_dirs = set() # type: Set[py.path.local] + for direntry in visit(str(argpath), self._recurse): + if not direntry.is_file(): + continue + + path = py.path.local(direntry.path) + dirpath = path.dirpath() + + if dirpath not in seen_dirs: + # Collect packages first. + seen_dirs.add(dirpath) + pkginit = dirpath.join("__init__.py") + if pkginit.exists(): + for x in self._collectfile(pkginit): + yield x + if isinstance(x, Package): + self._collection_pkg_roots[str(dirpath)] = x + if str(dirpath) in self._collection_pkg_roots: + # Do not collect packages here. + continue + + for x in self._collectfile(path): + key = (type(x), x.fspath) + if key in self._collection_node_cache2: + yield self._collection_node_cache2[key] + else: + self._collection_node_cache2[key] = x yield x - if isinstance(x, Package): - self._collection_pkg_roots[str(dirpath)] = x - if str(dirpath) in self._collection_pkg_roots: - # Do not collect packages here. + else: + assert argpath.check(file=1) + + if argpath in self._collection_node_cache1: + col = self._collection_node_cache1[argpath] + else: + collect_root = self._collection_pkg_roots.get(argpath.dirname, self) + col = collect_root._collectfile(argpath, handle_dupes=False) + if col: + self._collection_node_cache1[argpath] = col + m = self.matchnodes(col, names) + if not m: + report_arg = "::".join((str(argpath), *names)) + self._notfound.append((report_arg, col)) continue - for x in self._collectfile(path): - key = (type(x), x.fspath) - if key in self._collection_node_cache2: - yield self._collection_node_cache2[key] - else: - self._collection_node_cache2[key] = x - yield x - else: - assert argpath.check(file=1) + # If __init__.py was the only file requested, then the matched node will be + # the corresponding Package, and the first yielded item will be the __init__ + # Module itself, so just use that. If this special case isn't taken, then all + # the files in the package will be yielded. + if argpath.basename == "__init__.py": + assert isinstance(m[0], nodes.Collector) + try: + yield next(iter(m[0].collect())) + except StopIteration: + # The package collects nothing with only an __init__.py + # file in it, which gets ignored by the default + # "python_files" option. + pass + continue + yield from m - if argpath in self._collection_node_cache1: - col = self._collection_node_cache1[argpath] - else: - collect_root = self._collection_pkg_roots.get(argpath.dirname, self) - col = collect_root._collectfile(argpath, handle_dupes=False) - if col: - self._collection_node_cache1[argpath] = col - m = self.matchnodes(col, names) - if not m: - report_arg = "::".join((str(argpath), *names)) - self._notfound.append((report_arg, col)) - return - - # If __init__.py was the only file requested, then the matched node will be - # the corresponding Package, and the first yielded item will be the __init__ - # Module itself, so just use that. If this special case isn't taken, then all - # the files in the package will be yielded. - if argpath.basename == "__init__.py": - assert isinstance(m[0], nodes.Collector) - try: - yield next(iter(m[0].collect())) - except StopIteration: - # The package collects nothing with only an __init__.py - # file in it, which gets ignored by the default - # "python_files" option. - pass - return - yield from m + self.trace.root.indent -= 1 + self._collection_node_cache1.clear() + self._collection_node_cache2.clear() + self._collection_matchnodes_cache.clear() + self._collection_pkg_roots.clear() def matchnodes( self, From c867452488659729c2caef82cd323c79a2a19b08 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 22 Aug 2020 11:35:54 +0300 Subject: [PATCH 597/823] main: inline matchnodes() into collect() Now all of the logic is in one place and may be simplified and refactored in more sensible way. --- src/_pytest/main.py | 101 ++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index a581cbe23dc..69ea46a401b 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -707,8 +707,53 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: col = collect_root._collectfile(argpath, handle_dupes=False) if col: self._collection_node_cache1[argpath] = col - m = self.matchnodes(col, names) - if not m: + + matching = [] + work = [ + (col, names) + ] # type: List[Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]]] + while work: + self.trace("matchnodes", col, names) + self.trace.root.indent += 1 + + matchnodes, matchnames = work.pop() + for node in matchnodes: + if not matchnames: + matching.append(node) + continue + if not isinstance(node, nodes.Collector): + continue + key = (type(node), node.nodeid) + if key in self._collection_matchnodes_cache: + rep = self._collection_matchnodes_cache[key] + else: + rep = collect_one_node(node) + self._collection_matchnodes_cache[key] = rep + if rep.passed: + submatchnodes = [] + for r in rep.result: + # TODO: Remove parametrized workaround once collection structure contains + # parametrization. + if ( + r.name == matchnames[0] + or r.name.split("[")[0] == matchnames[0] + ): + submatchnodes.append(r) + if submatchnodes: + work.append((submatchnodes, matchnames[1:])) + # XXX Accept IDs that don't have "()" for class instances. + elif len(rep.result) == 1 and rep.result[0].name == "()": + work.append((rep.result, matchnames)) + else: + # Report collection failures here to avoid failing to run some test + # specified in the command line because the module could not be + # imported (#134). + node.ihook.pytest_collectreport(report=rep) + + self.trace("matchnodes finished -> ", len(matching), "nodes") + self.trace.root.indent -= 1 + + if not matching: report_arg = "::".join((str(argpath), *names)) self._notfound.append((report_arg, col)) continue @@ -718,16 +763,17 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # Module itself, so just use that. If this special case isn't taken, then all # the files in the package will be yielded. if argpath.basename == "__init__.py": - assert isinstance(m[0], nodes.Collector) + assert isinstance(matching[0], nodes.Collector) try: - yield next(iter(m[0].collect())) + yield next(iter(matching[0].collect())) except StopIteration: # The package collects nothing with only an __init__.py # file in it, which gets ignored by the default # "python_files" option. pass continue - yield from m + + yield from matching self.trace.root.indent -= 1 self._collection_node_cache1.clear() @@ -735,51 +781,6 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: self._collection_matchnodes_cache.clear() self._collection_pkg_roots.clear() - def matchnodes( - self, - matching: Sequence[Union[nodes.Item, nodes.Collector]], - names: Sequence[str], - ) -> Sequence[Union[nodes.Item, nodes.Collector]]: - result = [] - work = [(matching, names)] - while work: - self.trace("matchnodes", matching, names) - self.trace.root.indent += 1 - - matching, names = work.pop() - for node in matching: - if not names: - result.append(node) - continue - if not isinstance(node, nodes.Collector): - continue - key = (type(node), node.nodeid) - if key in self._collection_matchnodes_cache: - rep = self._collection_matchnodes_cache[key] - else: - rep = collect_one_node(node) - self._collection_matchnodes_cache[key] = rep - if rep.passed: - submatching = [] - for x in rep.result: - # TODO: Remove parametrized workaround once collection structure contains parametrization. - if x.name == names[0] or x.name.split("[")[0] == names[0]: - submatching.append(x) - if submatching: - work.append((submatching, names[1:])) - # XXX Accept IDs that don't have "()" for class instances. - elif len(rep.result) == 1 and rep.result[0].name == "()": - work.append((rep.result, names)) - else: - # Report collection failures here to avoid failing to run some test - # specified in the command line because the module could not be - # imported (#134). - node.ihook.pytest_collectreport(report=rep) - - self.trace("matchnodes finished -> ", len(result), "nodes") - self.trace.root.indent -= 1 - return result - def genitems( self, node: Union[nodes.Item, nodes.Collector] ) -> Iterator[nodes.Item]: From 023f0510afcd2a182423b45fa79da47c275ab446 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 22 Aug 2020 11:43:23 +0300 Subject: [PATCH 598/823] main: move collection cache attributes to local variables in collect() They are only used for the duration of this function. --- src/_pytest/main.py | 70 +++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 69ea46a401b..c4d1ba85ef4 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -45,8 +45,6 @@ from typing import Type from typing_extensions import Literal - from _pytest.python import Package - def pytest_addoption(parser: Parser) -> None: parser.addini( @@ -443,23 +441,6 @@ def __init__(self, config: Config) -> None: self.startdir = config.invocation_dir self._initialpaths = frozenset() # type: FrozenSet[py.path.local] - # Keep track of any collected nodes in here, so we don't duplicate fixtures. - self._collection_node_cache1 = ( - {} - ) # type: Dict[py.path.local, Sequence[nodes.Collector]] - self._collection_node_cache2 = ( - {} - ) # type: Dict[Tuple[Type[nodes.Collector], py.path.local], nodes.Collector] - - # Keep track of any collected collectors in matchnodes paths, so they - # are not collected more than once. - self._collection_matchnodes_cache = ( - {} - ) # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport] - - # Dirnames of pkgs with dunder-init files. - self._collection_pkg_roots = {} # type: Dict[str, Package] - self._bestrelpathcache = _bestrelpath_cache( config.rootdir ) # type: Dict[py.path.local, str] @@ -639,6 +620,21 @@ def perform_collect( # noqa: F811 def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: from _pytest.python import Package + # Keep track of any collected nodes in here, so we don't duplicate fixtures. + node_cache1 = {} # type: Dict[py.path.local, Sequence[nodes.Collector]] + node_cache2 = ( + {} + ) # type: Dict[Tuple[Type[nodes.Collector], py.path.local], nodes.Collector] + + # Keep track of any collected collectors in matchnodes paths, so they + # are not collected more than once. + matchnodes_cache = ( + {} + ) # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport] + + # Dirnames of pkgs with dunder-init files. + pkg_roots = {} # type: Dict[str, Package] + for argpath, names in self._initial_parts: self.trace("processing argument", (argpath, names)) self.trace.root.indent += 1 @@ -655,14 +651,12 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: if parent.isdir(): pkginit = parent.join("__init__.py") if pkginit.isfile(): - if pkginit not in self._collection_node_cache1: + if pkginit not in node_cache1: col = self._collectfile(pkginit, handle_dupes=False) if col: if isinstance(col[0], Package): - self._collection_pkg_roots[str(parent)] = col[0] - self._collection_node_cache1[col[0].fspath] = [ - col[0] - ] + pkg_roots[str(parent)] = col[0] + node_cache1[col[0].fspath] = [col[0]] # If it's a directory argument, recurse and look for any Subpackages. # Let the Package collector deal with subnodes, don't collect here. @@ -685,28 +679,28 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: for x in self._collectfile(pkginit): yield x if isinstance(x, Package): - self._collection_pkg_roots[str(dirpath)] = x - if str(dirpath) in self._collection_pkg_roots: + pkg_roots[str(dirpath)] = x + if str(dirpath) in pkg_roots: # Do not collect packages here. continue for x in self._collectfile(path): key = (type(x), x.fspath) - if key in self._collection_node_cache2: - yield self._collection_node_cache2[key] + if key in node_cache2: + yield node_cache2[key] else: - self._collection_node_cache2[key] = x + node_cache2[key] = x yield x else: assert argpath.check(file=1) - if argpath in self._collection_node_cache1: - col = self._collection_node_cache1[argpath] + if argpath in node_cache1: + col = node_cache1[argpath] else: - collect_root = self._collection_pkg_roots.get(argpath.dirname, self) + collect_root = pkg_roots.get(argpath.dirname, self) col = collect_root._collectfile(argpath, handle_dupes=False) if col: - self._collection_node_cache1[argpath] = col + node_cache1[argpath] = col matching = [] work = [ @@ -724,11 +718,11 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: if not isinstance(node, nodes.Collector): continue key = (type(node), node.nodeid) - if key in self._collection_matchnodes_cache: - rep = self._collection_matchnodes_cache[key] + if key in matchnodes_cache: + rep = matchnodes_cache[key] else: rep = collect_one_node(node) - self._collection_matchnodes_cache[key] = rep + matchnodes_cache[key] = rep if rep.passed: submatchnodes = [] for r in rep.result: @@ -776,10 +770,6 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: yield from matching self.trace.root.indent -= 1 - self._collection_node_cache1.clear() - self._collection_node_cache2.clear() - self._collection_matchnodes_cache.clear() - self._collection_pkg_roots.clear() def genitems( self, node: Union[nodes.Item, nodes.Collector] From c1f975668ec94ec6e93718abd696b7eb8450360b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 24 Aug 2020 11:14:57 +0300 Subject: [PATCH 599/823] main: couple of code simplifications --- src/_pytest/main.py | 18 ++++++++---------- src/_pytest/python.py | 5 ++--- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index c4d1ba85ef4..29b1e5f7006 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -511,9 +511,8 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if ihook.pytest_ignore_collect(path=path, config=self.config): return False norecursepatterns = self.config.getini("norecursedirs") - for pat in norecursepatterns: - if path.check(fnmatch=pat): - return False + if any(path.check(fnmatch=pat) for pat in norecursepatterns): + return False return True def _collectfile( @@ -650,13 +649,12 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: if parent.isdir(): pkginit = parent.join("__init__.py") - if pkginit.isfile(): - if pkginit not in node_cache1: - col = self._collectfile(pkginit, handle_dupes=False) - if col: - if isinstance(col[0], Package): - pkg_roots[str(parent)] = col[0] - node_cache1[col[0].fspath] = [col[0]] + if pkginit.isfile() and pkginit not in node_cache1: + col = self._collectfile(pkginit, handle_dupes=False) + if col: + if isinstance(col[0], Package): + pkg_roots[str(parent)] = col[0] + node_cache1[col[0].fspath] = [col[0]] # If it's a directory argument, recurse and look for any Subpackages. # Let the Package collector deal with subnodes, don't collect here. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 7aa5b422275..f792acbd855 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -644,9 +644,8 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if ihook.pytest_ignore_collect(path=path, config=self.config): return False norecursepatterns = self.config.getini("norecursedirs") - for pat in norecursepatterns: - if path.check(fnmatch=pat): - return False + if any(path.check(fnmatch=pat) for pat in norecursepatterns): + return False return True def _collectfile( From 00996adeb8b081f9c1c750066e0fa526532bb30e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Aug 2020 17:02:33 +0200 Subject: [PATCH 600/823] Update talks/trainings page (#7661) * Update talks/trainings page - Remove past webinar - Add new open training - Add some talks/webinars by Oliver Bestwalter and by me - Remove some stale link targets * Move sidebar to index --- doc/en/index.rst | 7 +++++++ doc/en/talks.rst | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index f1cb533d787..a9bc07fbd0c 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -1,5 +1,12 @@ :orphan: +.. sidebar:: Next Open Trainings + + - `pytest: Test Driven Development (nicht nur) für Python `_ (German) at the `CH Open Workshoptage `_, September 8 2020, HSLU Campus Rotkreuz (ZG), Switzerland. + - `Professional testing with Python `_, via Python Academy, February 1-3 2021, Leipzig (Germany) and remote. + + Also see `previous talks and blogposts `_. + .. _features: pytest: helps you write better programs diff --git a/doc/en/talks.rst b/doc/en/talks.rst index 253dfe78ed8..216ccb8dd8a 100644 --- a/doc/en/talks.rst +++ b/doc/en/talks.rst @@ -2,13 +2,6 @@ Talks and Tutorials ========================== -.. sidebar:: Next Open Trainings - - - `Free 1h webinar: "pytest: Test Driven Development für Python" `_ (German), online, August 18 2020. - - `"pytest: Test Driven Development (nicht nur) für Python" `_ (German) at the `CH Open Workshoptage `_, September 8 2020, HSLU Campus Rotkreuz (ZG), Switzerland. - -.. _`funcargs`: funcargs.html - Books --------------------------------------------- @@ -21,6 +14,16 @@ Books Talks and blog postings --------------------------------------------- +- Webinar: `pytest: Test Driven Development für Python (German) `_, Florian Bruhin, via mylearning.ch, 2020 + +- Webinar: `Simplify Your Tests with Fixtures `_, Oliver Bestwalter, via JetBrains, 2020 + +- Training: `Introduction to pytest - simple, rapid and fun testing with Python `_, Florian Bruhin, PyConDE 2019 + +- Abridged metaprogramming classics - this episode: pytest, Oliver Bestwalter, PyConDE 2019 (`repository `__, `recording `__) + +- Testing PySide/PyQt code easily using the pytest framework, Florian Bruhin, Qt World Summit 2019 (`slides `__, `recording `__) + - `pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyBCN June 2019 `_. - pytest: recommendations, basic packages for testing in Python and Django, Andreu Vallbona, PyconES 2017 (`slides in english `_, `video in spanish `_) @@ -52,8 +55,6 @@ Talks and blog postings - `pytest: helps you write better Django apps, Andreas Pelme, DjangoCon Europe 2014 `_. -- :ref:`fixtures` - - `Testing Django Applications with pytest, Andreas Pelme, EuroPython 2013 `_. From a267a622eb5af73f71f7bdf71ef83ed2ed27b782 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 25 Aug 2020 21:51:08 +0300 Subject: [PATCH 601/823] python: fix empty parametrize() leading to "NotSetType.token" id In ff8b7884e8f1019f60f270eab2c4909ff557dd4e NOTSET was changed to a singleton enum, which ended up unexpectedly triggering a code path in ID generation which checks for `isinstance(Enum)`. Add an explicit case for it, which is not too bad anyway. --- changelog/7686.bugfix.rst | 2 ++ src/_pytest/python.py | 3 +++ testing/python/metafunc.py | 9 +++++++++ 3 files changed, 14 insertions(+) create mode 100644 changelog/7686.bugfix.rst diff --git a/changelog/7686.bugfix.rst b/changelog/7686.bugfix.rst new file mode 100644 index 00000000000..8549fae8eb1 --- /dev/null +++ b/changelog/7686.bugfix.rst @@ -0,0 +1,2 @@ +Fixed `NotSetType.token` being used as the parameter ID when the parametrization list is empty. +Regressed in pytest 6.0.0. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index f792acbd855..21aa8457611 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1293,6 +1293,9 @@ def _idval( return str(val) elif isinstance(val, REGEX_TYPE): return ascii_escaped(val.pattern) + elif val is NOTSET: + # Fallback to default. Note that NOTSET is an enum.Enum. + pass elif isinstance(val, enum.Enum): return str(val) elif isinstance(getattr(val, "__name__", None), str): diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 7aa608a0423..6b59104567a 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -21,6 +21,7 @@ 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 from _pytest.pytester import Testdir from _pytest.python import _idval @@ -359,6 +360,14 @@ def test_function(): for val, expected in values: assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected + def test_notset_idval(self) -> None: + """Test that a NOTSET value (used by an empty parameterset) generates + a proper ID. + + Regression test for #7686. + """ + assert _idval(NOTSET, "a", 0, None, nodeid=None, config=None) == "a0" + def test_idmaker_autoname(self) -> None: """#250""" result = idmaker( From 98891a59479e3de6fe4cabc770aaad894211822a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 21 Aug 2020 17:27:06 +0300 Subject: [PATCH 602/823] python: skip pytest_pycollect_makeitem work on certain names When a Python object (module/class/instance) is collected, for each name in `obj.__dict__` (and up its MRO) the pytest_pycollect_makeitem hook is called for potentially creating a node for it. These Python objects have a bunch of builtin attributes that are extremely unlikely to be collected. But due to their pervasiveness, dispatching the hook for them ends up being mildly expensive and also pollutes PYTEST_DEBUG=1 output and such. Let's just ignore these attributes. On the pandas test suite commit 04e9e0afd476b1b8bed930e47bf60e, collect only, irrelevant lines snipped, about 5% improvement: Before: ``` 51195095 function calls (48844352 primitive calls) in 39.089 seconds ncalls tottime percall cumtime percall filename:lineno(function) 226602/54 0.145 0.000 38.940 0.721 manager.py:90(_hookexec) 72227 0.285 0.000 20.146 0.000 python.py:424(_makeitem) 72227 0.171 0.000 16.678 0.000 python.py:218(pytest_pycollect_makeitem) ``` After: ``` 48410921 function calls (46240870 primitive calls) in 36.950 seconds ncalls tottime percall cumtime percall filename:lineno(function) 181429/54 0.113 0.000 36.777 0.681 manager.py:90(_hookexec) 27054 0.130 0.000 17.755 0.001 python.py:465(_makeitem) 27054 0.121 0.000 16.219 0.001 python.py:218(pytest_pycollect_makeitem) ``` --- changelog/7671.trivial.rst | 6 ++++++ src/_pytest/python.py | 23 +++++++++++++++++++++++ testing/python/collect.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 changelog/7671.trivial.rst diff --git a/changelog/7671.trivial.rst b/changelog/7671.trivial.rst new file mode 100644 index 00000000000..6dddf4cf042 --- /dev/null +++ b/changelog/7671.trivial.rst @@ -0,0 +1,6 @@ +When collecting tests, pytest finds test classes and functions by examining the +attributes of python objects (modules, classes and instances). To speed up this +process, pytest now ignores builtin attributes (like ``__class__``, +``__delattr__`` and ``__new__``) without consulting the ``python_classes`` and +``python_functions`` configuration options and without passing them to plugins +using the ``pytest_pycollect_makeitem`` hook. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 21aa8457611..be21b61d61e 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -5,6 +5,7 @@ import itertools import os import sys +import types import typing import warnings from collections import Counter @@ -343,6 +344,26 @@ def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]: return fspath, lineno, modpath +# As an optimization, these builtin attribute names are pre-ignored when +# iterating over an object during collection -- the pytest_pycollect_makeitem +# hook is not called for them. +# fmt: off +class _EmptyClass: pass # noqa: E701 +IGNORED_ATTRIBUTES = frozenset.union( # noqa: E305 + frozenset(), + # Module. + dir(types.ModuleType("empty_module")), + # Some extra module attributes the above doesn't catch. + {"__builtins__", "__file__", "__cached__"}, + # Class. + dir(_EmptyClass), + # Instance. + dir(_EmptyClass()), +) +del _EmptyClass +# fmt: on + + class PyCollector(PyobjMixin, nodes.Collector): def funcnamefilter(self, name: str) -> bool: return self._matches_prefix_or_glob_option("python_functions", name) @@ -404,6 +425,8 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: # Note: seems like the dict can change during iteration - # be careful not to remove the list() without consideration. for name, obj in list(dic.items()): + if name in IGNORED_ATTRIBUTES: + continue if name in seen: continue seen.add(name) diff --git a/testing/python/collect.py b/testing/python/collect.py index 778ceeddf8d..ab4a6fbb832 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -885,6 +885,34 @@ def test_something(): result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 passed*"]) + def test_early_ignored_attributes(self, testdir: Testdir) -> None: + """Builtin attributes should be ignored early on, even if + configuration would otherwise allow them. + + This tests a performance optimization, not correctness, really, + although it tests PytestCollectionWarning is not raised, while + it would have been raised otherwise. + """ + testdir.makeini( + """ + [pytest] + python_classes=* + python_functions=* + """ + ) + testdir.makepyfile( + """ + class TestEmpty: + pass + test_empty = TestEmpty() + def test_real(): + pass + """ + ) + items, rec = testdir.inline_genitems() + assert rec.ret == 0 + assert len(items) == 1 + def test_setup_only_available_in_subdir(testdir): sub1 = testdir.mkpydir("sub1") From daca174c987389e9cc1a95ce9497dffc6ce51020 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 26 Aug 2020 18:05:08 +0300 Subject: [PATCH 603/823] python: small optimization in PyCollector.collect() Inline `_makeitem()` so that `self.ihook` (which is moderately expensive) can be called only once. Note: the removed test "test_makeitem_non_underscore" comes from an old behavior of skipping names that start with `_` which has since been generalized, making the test no longer relevant. --- src/_pytest/python.py | 23 ++++++++--------------- testing/python/collect.py | 9 --------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index be21b61d61e..eeccb475555 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -421,6 +421,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: dicts.append(basecls.__dict__) seen = set() # type: Set[str] values = [] # type: List[Union[nodes.Item, nodes.Collector]] + ihook = self.ihook for dic in dicts: # Note: seems like the dict can change during iteration - # be careful not to remove the list() without consideration. @@ -430,12 +431,15 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: if name in seen: continue seen.add(name) - res = self._makeitem(name, obj) + res = ihook.pytest_pycollect_makeitem( + collector=self, name=name, obj=obj + ) if res is None: continue - if not isinstance(res, list): - res = [res] - values.extend(res) + elif isinstance(res, list): + values.extend(res) + else: + values.append(res) def sort_key(item): fspath, lineno, _ = item.reportinfo() @@ -444,17 +448,6 @@ def sort_key(item): values.sort(key=sort_key) return values - def _makeitem( - self, name: str, obj: object - ) -> Union[ - None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]] - ]: - # assert self.ihook.fspath == self.fspath, self - item = self.ihook.pytest_pycollect_makeitem( - collector=self, name=name, obj=obj - ) # type: Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]] - return item - def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]: modulecol = self.getparent(Module) assert modulecol is not None diff --git a/testing/python/collect.py b/testing/python/collect.py index ab4a6fbb832..01294039860 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -843,15 +843,6 @@ def pytest_pycollect_makeitem(collector, name, obj): result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines(["*MyFunction*some*"]) - def test_makeitem_non_underscore(self, testdir, monkeypatch): - modcol = testdir.getmodulecol("def _hello(): pass") - values = [] - monkeypatch.setattr( - pytest.Module, "_makeitem", lambda self, name, obj: values.append(name) - ) - values = modcol.collect() - assert "_hello" not in values - def test_issue2369_collect_module_fileext(self, testdir): """Ensure we can collect files with weird file extensions as Python modules (#2369)""" From 12de92cd2b818906d342dbdfaf96999887bc9658 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 28 Aug 2020 09:55:26 +0300 Subject: [PATCH 604/823] fixture: remove `@scopeproperty` I think the straight code is easier to understand. --- src/_pytest/fixtures.py | 47 ++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 47a7ac2253e..d2a08d57be0 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -117,29 +117,6 @@ def pytest_sessionstart(session: "Session") -> None: scopename2class = {} # type: Dict[str, Type[nodes.Node]] -scope2props = dict(session=()) # type: Dict[str, Tuple[str, ...]] -scope2props["package"] = ("fspath",) -scope2props["module"] = ("fspath", "module") -scope2props["class"] = scope2props["module"] + ("cls",) -scope2props["instance"] = scope2props["class"] + ("instance",) -scope2props["function"] = scope2props["instance"] + ("function", "keywords") - - -def scopeproperty(name=None, doc=None): - def decoratescope(func): - scopename = name or func.__name__ - - def provide(self): - if func.__name__ in scope2props[self.scope]: - return func(self) - raise AttributeError( - "{} not available in {}-scoped context".format(scopename, self.scope) - ) - - return property(provide, None, None, func.__doc__) - - return decoratescope - def get_scope_package(node, fixturedef: "FixtureDef[object]"): import pytest @@ -484,14 +461,22 @@ def config(self) -> Config: """The pytest config object associated with this request.""" return self._pyfuncitem.config # type: ignore[no-any-return] # noqa: F723 - @scopeproperty() + @property def function(self): """Test function object if the request has a per-function scope.""" + if self.scope != "function": + raise AttributeError( + "function not available in {}-scoped context".format(self.scope) + ) return self._pyfuncitem.obj - @scopeproperty("class") + @property def cls(self): """Class (can be None) where the test function was collected.""" + if self.scope not in ("class", "function"): + raise AttributeError( + "cls not available in {}-scoped context".format(self.scope) + ) clscol = self._pyfuncitem.getparent(_pytest.python.Class) if clscol: return clscol.obj @@ -506,14 +491,22 @@ def instance(self): function = getattr(self, "function", None) return getattr(function, "__self__", None) - @scopeproperty() + @property def module(self): """Python module object where the test function was collected.""" + if self.scope not in ("function", "class", "module"): + raise AttributeError( + "module not available in {}-scoped context".format(self.scope) + ) return self._pyfuncitem.getparent(_pytest.python.Module).obj - @scopeproperty() + @property def fspath(self) -> py.path.local: """The file system path of the test module which collected this test.""" + if self.scope not in ("function", "class", "module", "package"): + raise AttributeError( + "module not available in {}-scoped context".format(self.scope) + ) # TODO: Remove ignore once _pyfuncitem is properly typed. return self._pyfuncitem.fspath # type: ignore From ceea6000ba26629d11b86cc1fb33d5cb8fd758b7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 27 Aug 2020 19:49:58 -0300 Subject: [PATCH 605/823] Add missing File reference to the docs As related in #7696 --- doc/en/reference.rst | 11 +++++++++-- src/_pytest/nodes.py | 5 ++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index eb2370ae48d..313c76c4c86 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -784,12 +784,19 @@ ExceptionInfo :members: -pytest.ExitCode -~~~~~~~~~~~~~~~ +ExitCode +~~~~~~~~ .. autoclass:: _pytest.config.ExitCode :members: +File +~~~~ + +.. autoclass:: _pytest.nodes.File() + :members: + :show-inheritance: + FixtureDef ~~~~~~~~~~ diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 26fab67fe68..5dde4c7370e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -542,7 +542,10 @@ def isinitpath(self, path: py.path.local) -> bool: class File(FSCollector): - """Base class for collecting tests from a file.""" + """Base class for collecting tests from a file. + + :ref:`non-python tests`. + """ class Item(Node): From 9f672c85c514e08949c36fd8292dc8e248b9461f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 3 Sep 2020 07:14:32 -0300 Subject: [PATCH 606/823] Fix handle of exceptions in ReprEntry with tb=line Fix #7707 --- changelog/7707.bugfix.rst | 1 + src/_pytest/_code/code.py | 24 +++++++++------------ testing/code/test_excinfo.py | 42 +++++++++++++++++++++--------------- 3 files changed, 36 insertions(+), 31 deletions(-) create mode 100644 changelog/7707.bugfix.rst diff --git a/changelog/7707.bugfix.rst b/changelog/7707.bugfix.rst new file mode 100644 index 00000000000..fbe979d9d6a --- /dev/null +++ b/changelog/7707.bugfix.rst @@ -0,0 +1 @@ +Fix internal error when handling some exceptions that contain multiple lines or the style uses multiple lines (``--tb=line`` for example). diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 96fa9199b7e..12d39306a69 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1049,25 +1049,21 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None: # such as "> assert 0" fail_marker = "{} ".format(FormattedExcinfo.fail_marker) indent_size = len(fail_marker) - indents = [] - source_lines = [] - failure_lines = [] - seeing_failures = False - for line in self.lines: - is_source_line = not line.startswith(fail_marker) - if is_source_line: - assert not seeing_failures, ( - "Unexpected failure lines between source lines:\n" - + "\n".join(self.lines) - ) + indents = [] # type: List[str] + source_lines = [] # type: List[str] + failure_lines = [] # type: List[str] + for index, line in enumerate(self.lines): + is_failure_line = line.startswith(fail_marker) + if is_failure_line: + # from this point on all lines are considered part of the failure + failure_lines.extend(self.lines[index:]) + break + else: if self.style == "value": source_lines.append(line) else: indents.append(line[:indent_size]) source_lines.append(line[indent_size:]) - else: - seeing_failures = True - failure_lines.append(line) tw._write_source(source_lines, indents) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 75446c570b2..4dfd6f5cc95 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -4,6 +4,8 @@ import queue import sys import textwrap +from typing import Any +from typing import Dict from typing import Tuple from typing import Union @@ -1045,28 +1047,34 @@ def f(): @pytest.mark.parametrize( "reproptions", [ - { - "style": style, - "showlocals": showlocals, - "funcargs": funcargs, - "tbfilter": tbfilter, - } - for style in ("long", "short", "no") + pytest.param( + { + "style": style, + "showlocals": showlocals, + "funcargs": funcargs, + "tbfilter": tbfilter, + }, + id="style={},showlocals={},funcargs={},tbfilter={}".format( + style, showlocals, funcargs, tbfilter + ), + ) + for style in ["long", "short", "line", "no", "native", "value", "auto"] for showlocals in (True, False) for tbfilter in (True, False) for funcargs in (True, False) ], ) - def test_format_excinfo(self, importasmod, reproptions): - mod = importasmod( - """ - def g(x): - raise ValueError(x) - def f(): - g(3) - """ - ) - excinfo = pytest.raises(ValueError, mod.f) + def test_format_excinfo(self, reproptions: Dict[str, Any]) -> None: + def bar(): + assert False, "some error" + + def foo(): + bar() + + # using inline functions as opposed to importasmod so we get source code lines + # in the tracebacks (otherwise getinspect doesn't find the source code). + with pytest.raises(AssertionError) as excinfo: + foo() file = io.StringIO() tw = TerminalWriter(file=file) repr = excinfo.getrepr(**reproptions) From 19e99ab4131fbd709ce0e5a17694e1be3b22f355 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 4 Sep 2020 11:57:15 -0300 Subject: [PATCH 607/823] Integrate warnings filtering directly into Config (#7700) Warnings are a central part of Python, so much that Python itself has command-line and environtment variables to handle warnings. By moving the concept of warning handling into Config, it becomes natural to filter warnings issued as early as possible, even before the "_pytest.warnings" plugin is given a chance to spring into action. This also avoids the weird coupling between config and the warnings plugin that was required before. Fix #6681 Fix #2891 Fix #7620 Fix #7626 Close #7649 Co-authored-by: Ran Benita --- changelog/6681.improvement.rst | 3 + src/_pytest/assertion/rewrite.py | 4 +- src/_pytest/config/__init__.py | 156 +++++++++++++++++++++++++------ src/_pytest/faulthandler.py | 5 +- src/_pytest/main.py | 14 +++ src/_pytest/warnings.py | 109 +++------------------ testing/test_config.py | 109 ++++++++++++++++++--- testing/test_warnings.py | 23 +++-- 8 files changed, 277 insertions(+), 146 deletions(-) create mode 100644 changelog/6681.improvement.rst diff --git a/changelog/6681.improvement.rst b/changelog/6681.improvement.rst new file mode 100644 index 00000000000..cc586e6a337 --- /dev/null +++ b/changelog/6681.improvement.rst @@ -0,0 +1,3 @@ +Internal pytest warnings issued during the early stages of initialization are now properly handled and can filtered through :confval:`filterwarnings` or ``--pythonwarnings/-W``. + +This also fixes a number of long standing issues: `#2891 `__, `#7620 `__, `#7426 `__. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 50fa4d405a2..48587b17e89 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -267,13 +267,11 @@ def mark_rewrite(self, *names: str) -> None: def _warn_already_imported(self, name: str) -> None: from _pytest.warning_types import PytestAssertRewriteWarning - from _pytest.warnings import _issue_warning_captured - _issue_warning_captured( + self.config.issue_config_time_warning( PytestAssertRewriteWarning( "Module already imported so cannot be rewritten: %s" % name ), - self.config.hook, stacklevel=5, ) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 1c6ad32882d..5949c787bbc 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -6,6 +6,7 @@ import enum import inspect import os +import re import shlex import sys import types @@ -15,6 +16,7 @@ from typing import Any from typing import Callable from typing import Dict +from typing import Generator from typing import IO from typing import Iterable from typing import Iterator @@ -342,6 +344,13 @@ def __init__(self) -> None: self._noconftest = False self._duplicatepaths = set() # type: Set[py.path.local] + # plugins that were explicitly skipped with pytest.skip + # list of (module name, skip reason) + # previously we would issue a warning when a plugin was skipped, but + # since we refactored warnings as first citizens of Config, they are + # just stored here to be used later. + self.skipped_plugins = [] # type: List[Tuple[str, str]] + self.add_hookspecs(_pytest.hookspec) self.register(self) if os.environ.get("PYTEST_DEBUG"): @@ -694,13 +703,7 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No ).with_traceback(e.__traceback__) from e except Skipped as e: - from _pytest.warnings import _issue_warning_captured - - _issue_warning_captured( - PytestConfigWarning("skipped plugin {!r}: {}".format(modname, e.msg)), - self.hook, - stacklevel=2, - ) + self.skipped_plugins.append((modname, e.msg or "")) else: mod = sys.modules[importspec] self.register(mod, modname) @@ -1092,6 +1095,9 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: self._validate_args(self.getini("addopts"), "via addopts config") + args ) + self.known_args_namespace = self._parser.parse_known_args( + args, namespace=copy.copy(self.option) + ) self._checkversion() self._consider_importhook(args) self.pluginmanager.consider_preparse(args, exclude_only=False) @@ -1100,10 +1106,10 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: # plugins are going to be loaded. self.pluginmanager.load_setuptools_entrypoints("pytest11") self.pluginmanager.consider_env() - self.known_args_namespace = ns = self._parser.parse_known_args( - args, namespace=copy.copy(self.option) - ) + self._validate_plugins() + self._warn_about_skipped_plugins() + if self.known_args_namespace.confcutdir is None and self.inifile: confcutdir = py.path.local(self.inifile).dirname self.known_args_namespace.confcutdir = confcutdir @@ -1112,21 +1118,24 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: early_config=self, args=args, parser=self._parser ) except ConftestImportFailure as e: - if ns.help or ns.version: + if self.known_args_namespace.help or self.known_args_namespace.version: # we don't want to prevent --help/--version to work # so just let is pass and print a warning at the end - from _pytest.warnings import _issue_warning_captured - - _issue_warning_captured( + self.issue_config_time_warning( PytestConfigWarning( "could not load initial conftests: {}".format(e.path) ), - self.hook, stacklevel=2, ) else: raise - self._validate_keys() + + @hookimpl(hookwrapper=True) + def pytest_collection(self) -> Generator[None, None, None]: + """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() def _checkversion(self) -> None: import pytest @@ -1147,9 +1156,9 @@ def _checkversion(self) -> None: % (self.inifile, minver, pytest.__version__,) ) - def _validate_keys(self) -> None: + def _validate_config_options(self) -> None: for key in sorted(self._get_unknown_ini_keys()): - self._warn_or_fail_if_strict("Unknown config ini key: {}\n".format(key)) + self._warn_or_fail_if_strict("Unknown config option: {}\n".format(key)) def _validate_plugins(self) -> None: required_plugins = sorted(self.getini("required_plugins")) @@ -1165,7 +1174,6 @@ def _validate_plugins(self) -> None: missing_plugins = [] for required_plugin in required_plugins: - spec = None try: spec = Requirement(required_plugin) except InvalidRequirement: @@ -1187,11 +1195,7 @@ def _warn_or_fail_if_strict(self, message: str) -> None: if self.known_args_namespace.strict_config: fail(message, pytrace=False) - from _pytest.warnings import _issue_warning_captured - - _issue_warning_captured( - PytestConfigWarning(message), self.hook, stacklevel=3, - ) + self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3) def _get_unknown_ini_keys(self) -> List[str]: parser_inicfg = self._parser._inidict @@ -1222,6 +1226,49 @@ def parse(self, args: List[str], addopts: bool = True) -> None: except PrintHelp: pass + def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None: + """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``. + + This function is mainly intended for plugins that need to issue warnings during + ``pytest_configure`` (or similar stages). + + :param warning: The warning instance. + :param stacklevel: stacklevel forwarded to warnings.warn. + """ + if self.pluginmanager.is_blocked("warnings"): + return + + cmdline_filters = self.known_args_namespace.pythonwarnings or [] + config_filters = self.getini("filterwarnings") + + with warnings.catch_warnings(record=True) as records: + warnings.simplefilter("always", type(warning)) + apply_warning_filters(config_filters, cmdline_filters) + warnings.warn(warning, stacklevel=stacklevel) + + if records: + frame = sys._getframe(stacklevel - 1) + location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + self.hook.pytest_warning_captured.call_historic( + kwargs=dict( + warning_message=records[0], + when="config", + item=None, + location=location, + ) + ) + self.hook.pytest_warning_recorded.call_historic( + kwargs=dict( + warning_message=records[0], + when="config", + nodeid="", + location=location, + ) + ) + def addinivalue_line(self, name: str, line: str) -> None: """Add a line to an ini-file option. The option must have been declared but might not yet be set in which case the line becomes @@ -1365,8 +1412,6 @@ def getvalueorskip(self, name: str, path=None): def _warn_about_missing_assertion(self, mode: str) -> None: if not _assertion_supported(): - from _pytest.warnings import _issue_warning_captured - if mode == "plain": warning_text = ( "ASSERTIONS ARE NOT EXECUTED" @@ -1381,8 +1426,15 @@ def _warn_about_missing_assertion(self, mode: str) -> None: "by the underlying Python interpreter " "(are you using python -O?)\n" ) - _issue_warning_captured( - PytestConfigWarning(warning_text), self.hook, stacklevel=3, + self.issue_config_time_warning( + PytestConfigWarning(warning_text), stacklevel=3, + ) + + def _warn_about_skipped_plugins(self) -> None: + for module_name, msg in self.pluginmanager.skipped_plugins: + self.issue_config_time_warning( + PytestConfigWarning("skipped plugin {!r}: {}".format(module_name, msg)), + stacklevel=2, ) @@ -1435,3 +1487,51 @@ def _strtobool(val: str) -> bool: return False else: raise ValueError("invalid truth value {!r}".format(val)) + + +@lru_cache(maxsize=50) +def parse_warning_filter( + arg: str, *, escape: bool +) -> "Tuple[str, str, Type[Warning], str, int]": + """Parse a warnings filter string. + + This is copied from warnings._setoption, but does not apply the filter, + only parses it, and makes the escaping optional. + """ + parts = arg.split(":") + if len(parts) > 5: + raise warnings._OptionError("too many fields (max 5): {!r}".format(arg)) + while len(parts) < 5: + parts.append("") + action_, message, category_, module, lineno_ = [s.strip() for s in parts] + action = warnings._getaction(action_) # type: str # type: ignore[attr-defined] + category = warnings._getcategory( + category_ + ) # type: Type[Warning] # type: ignore[attr-defined] + if message and escape: + message = re.escape(message) + if module and escape: + module = re.escape(module) + r"\Z" + if lineno_: + try: + lineno = int(lineno_) + if lineno < 0: + raise ValueError + except (ValueError, OverflowError) as e: + raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e + else: + lineno = 0 + return action, message, category, module, lineno + + +def apply_warning_filters( + config_filters: Iterable[str], cmdline_filters: Iterable[str] +) -> None: + """Applies pytest-configured filters to the warnings module""" + # Filters should have this precedence: cmdline options, config. + # Filters should be applied in the inverse order of precedence. + for arg in config_filters: + warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) + + for arg in cmdline_filters: + warnings.filterwarnings(*parse_warning_filter(arg, escape=True)) diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index e4a952966af..d0cc0430c49 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -30,18 +30,15 @@ def pytest_configure(config: Config) -> None: # of enabling faulthandler before each test executes. config.pluginmanager.register(FaultHandlerHooks(), "faulthandler-hooks") else: - from _pytest.warnings import _issue_warning_captured - # Do not handle dumping to stderr if faulthandler is already enabled, so warn # users that the option is being ignored. timeout = FaultHandlerHooks.get_timeout_config_value(config) if timeout > 0: - _issue_warning_captured( + config.issue_config_time_warning( pytest.PytestConfigWarning( "faulthandler module enabled before pytest configuration step, " "'faulthandler_timeout' option ignored" ), - config.hook, stacklevel=2, ) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 29b1e5f7006..4ab91f82c07 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -69,6 +69,20 @@ def pytest_addoption(parser: Parser) -> None: const=1, help="exit instantly on first error or failed test.", ) + group = parser.getgroup("pytest-warnings") + group.addoption( + "-W", + "--pythonwarnings", + action="append", + help="set which warnings to report, see -W option of python itself.", + ) + parser.addini( + "filterwarnings", + type="linelist", + help="Each line specifies a pattern for " + "warnings.filterwarnings. " + "Processed after -W/--pythonwarnings.", + ) group._addoption( "--maxfail", metavar="num", diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 4478d8723c6..950d0bb3859 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -1,77 +1,22 @@ -import re import sys import warnings from contextlib import contextmanager -from functools import lru_cache from typing import Generator from typing import Optional -from typing import Tuple import pytest from _pytest.compat import TYPE_CHECKING +from _pytest.config import apply_warning_filters from _pytest.config import Config -from _pytest.config.argparsing import Parser +from _pytest.config import parse_warning_filter from _pytest.main import Session from _pytest.nodes import Item from _pytest.terminal import TerminalReporter if TYPE_CHECKING: - from typing import Type from typing_extensions import Literal -@lru_cache(maxsize=50) -def _parse_filter( - arg: str, *, escape: bool -) -> "Tuple[str, str, Type[Warning], str, int]": - """Parse a warnings filter string. - - This is copied from warnings._setoption, but does not apply the filter, - only parses it, and makes the escaping optional. - """ - parts = arg.split(":") - if len(parts) > 5: - raise warnings._OptionError("too many fields (max 5): {!r}".format(arg)) - while len(parts) < 5: - parts.append("") - action_, message, category_, module, lineno_ = [s.strip() for s in parts] - action = warnings._getaction(action_) # type: str # type: ignore[attr-defined] - category = warnings._getcategory( - category_ - ) # type: Type[Warning] # type: ignore[attr-defined] - if message and escape: - message = re.escape(message) - if module and escape: - module = re.escape(module) + r"\Z" - if lineno_: - try: - lineno = int(lineno_) - if lineno < 0: - raise ValueError - except (ValueError, OverflowError) as e: - raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e - else: - lineno = 0 - return (action, message, category, module, lineno) - - -def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("pytest-warnings") - group.addoption( - "-W", - "--pythonwarnings", - action="append", - help="set which warnings to report, see -W option of python itself.", - ) - parser.addini( - "filterwarnings", - type="linelist", - help="Each line specifies a pattern for " - "warnings.filterwarnings. " - "Processed after -W/--pythonwarnings.", - ) - - def pytest_configure(config: Config) -> None: config.addinivalue_line( "markers", @@ -93,8 +38,8 @@ def catch_warnings_for_item( Each warning captured triggers the ``pytest_warning_recorded`` hook. """ - cmdline_filters = config.getoption("pythonwarnings") or [] - inifilters = config.getini("filterwarnings") + config_filters = config.getini("filterwarnings") + cmdline_filters = config.known_args_namespace.pythonwarnings or [] with warnings.catch_warnings(record=True) as log: # mypy can't infer that record=True means log is not None; help it. assert log is not None @@ -104,19 +49,14 @@ def catch_warnings_for_item( warnings.filterwarnings("always", category=DeprecationWarning) warnings.filterwarnings("always", category=PendingDeprecationWarning) - # Filters should have this precedence: mark, cmdline options, ini. - # Filters should be applied in the inverse order of precedence. - for arg in inifilters: - warnings.filterwarnings(*_parse_filter(arg, escape=False)) - - for arg in cmdline_filters: - warnings.filterwarnings(*_parse_filter(arg, escape=True)) + apply_warning_filters(config_filters, cmdline_filters) + # apply filters from "filterwarnings" marks nodeid = "" if item is None else item.nodeid if item is not None: for mark in item.iter_markers(name="filterwarnings"): for arg in mark.args: - warnings.filterwarnings(*_parse_filter(arg, escape=False)) + warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) yield @@ -189,30 +129,11 @@ def pytest_sessionfinish(session: Session) -> Generator[None, None, None]: yield -def _issue_warning_captured(warning: Warning, hook, stacklevel: int) -> None: - """A function that should be used instead of calling ``warnings.warn`` - directly when we are in the "configure" stage. - - At this point the actual options might not have been set, so we manually - trigger the pytest_warning_recorded hook so we can display these warnings - in the terminal. This is a hack until we can sort out #2891. - - :param warning: The warning instance. - :param hook: The hook caller. - :param stacklevel: stacklevel forwarded to warnings.warn. - """ - with warnings.catch_warnings(record=True) as records: - warnings.simplefilter("always", type(warning)) - warnings.warn(warning, stacklevel=stacklevel) - frame = sys._getframe(stacklevel - 1) - location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name - hook.pytest_warning_captured.call_historic( - kwargs=dict( - warning_message=records[0], when="config", item=None, location=location - ) - ) - hook.pytest_warning_recorded.call_historic( - kwargs=dict( - warning_message=records[0], when="config", nodeid="", location=location - ) - ) +@pytest.hookimpl(hookwrapper=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 diff --git a/testing/test_config.py b/testing/test_config.py index 346edb3304c..64186ffcbef 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -5,6 +5,7 @@ from typing import Dict from typing import List from typing import Sequence +from typing import Tuple import attr import py.path @@ -12,11 +13,14 @@ import _pytest._code import pytest from _pytest.compat import importlib_metadata +from _pytest.compat import TYPE_CHECKING from _pytest.config import _get_plugin_specs_as_list from _pytest.config import _iter_rewritable_modules +from _pytest.config import _strtobool from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.config import ExitCode +from _pytest.config import parse_warning_filter from _pytest.config.exceptions import UsageError from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor @@ -25,6 +29,9 @@ from _pytest.pathlib import Path from _pytest.pytester import Testdir +if TYPE_CHECKING: + from typing import Type + class TestParseIni: @pytest.mark.parametrize( @@ -183,10 +190,10 @@ def test_confcutdir(self, testdir): ["unknown_ini", "another_unknown_ini"], [ "=*= warnings summary =*=", - "*PytestConfigWarning:*Unknown config ini key: another_unknown_ini", - "*PytestConfigWarning:*Unknown config ini key: unknown_ini", + "*PytestConfigWarning:*Unknown config option: another_unknown_ini", + "*PytestConfigWarning:*Unknown config option: unknown_ini", ], - "Unknown config ini key: another_unknown_ini", + "Unknown config option: another_unknown_ini", ), ( """ @@ -197,9 +204,9 @@ def test_confcutdir(self, testdir): ["unknown_ini"], [ "=*= warnings summary =*=", - "*PytestConfigWarning:*Unknown config ini key: unknown_ini", + "*PytestConfigWarning:*Unknown config option: unknown_ini", ], - "Unknown config ini key: unknown_ini", + "Unknown config option: unknown_ini", ), ( """ @@ -232,7 +239,8 @@ def test_confcutdir(self, testdir): ), ], ) - def test_invalid_ini_keys( + @pytest.mark.filterwarnings("default") + def test_invalid_config_options( self, testdir, ini_file_text, invalid_keys, warning_output, exception_text ): testdir.makeconftest( @@ -250,10 +258,40 @@ def pytest_addoption(parser): result.stdout.fnmatch_lines(warning_output) if exception_text: - with pytest.raises(pytest.fail.Exception, match=exception_text): - testdir.runpytest("--strict-config") - else: - testdir.runpytest("--strict-config") + result = testdir.runpytest("--strict-config") + result.stdout.fnmatch_lines("INTERNALERROR>*" + exception_text) + + @pytest.mark.filterwarnings("default") + def test_silence_unknown_key_warning(self, testdir: Testdir) -> None: + """Unknown config key warnings can be silenced using filterwarnings (#7620)""" + testdir.makeini( + """ + [pytest] + filterwarnings = + ignore:Unknown config option:pytest.PytestConfigWarning + foobar=1 + """ + ) + result = testdir.runpytest() + result.stdout.no_fnmatch_line("*PytestConfigWarning*") + + @pytest.mark.filterwarnings("default") + def test_disable_warnings_plugin_disables_config_warnings( + self, testdir: Testdir + ) -> None: + """Disabling 'warnings' plugin also disables config time warnings""" + testdir.makeconftest( + """ + import pytest + def pytest_configure(config): + config.issue_config_time_warning( + pytest.PytestConfigWarning("custom config warning"), + stacklevel=2, + ) + """ + ) + result = testdir.runpytest("-pno:warnings") + result.stdout.no_fnmatch_line("*PytestConfigWarning*") @pytest.mark.parametrize( "ini_file_text, exception_text", @@ -1132,7 +1170,7 @@ def pytest_load_initial_conftests(self): pm.register(m) hc = pm.hook.pytest_load_initial_conftests values = hc._nonwrappers + hc._wrappers - expected = ["_pytest.config", m.__module__, "_pytest.capture"] + expected = ["_pytest.config", m.__module__, "_pytest.capture", "_pytest.warnings"] assert [x.function.__module__ for x in values] == expected @@ -1816,3 +1854,52 @@ def test_conftest_import_error_repr(tmpdir): assert exc.__traceback__ is not None exc_info = (type(exc), exc, exc.__traceback__) raise ConftestImportFailure(path, exc_info) from exc + + +def test_strtobool(): + assert _strtobool("YES") + assert not _strtobool("NO") + with pytest.raises(ValueError): + _strtobool("unknown") + + +@pytest.mark.parametrize( + "arg, escape, expected", + [ + ("ignore", False, ("ignore", "", Warning, "", 0)), + ( + "ignore::DeprecationWarning", + False, + ("ignore", "", DeprecationWarning, "", 0), + ), + ( + "ignore:some msg:DeprecationWarning", + False, + ("ignore", "some msg", DeprecationWarning, "", 0), + ), + ( + "ignore::DeprecationWarning:mod", + False, + ("ignore", "", DeprecationWarning, "mod", 0), + ), + ( + "ignore::DeprecationWarning:mod:42", + False, + ("ignore", "", DeprecationWarning, "mod", 42), + ), + ("error:some\\msg:::", True, ("error", "some\\\\msg", Warning, "", 0)), + ("error:::mod\\foo:", True, ("error", "", Warning, "mod\\\\foo\\Z", 0)), + ], +) +def test_parse_warning_filter( + arg: str, escape: bool, expected: "Tuple[str, str, Type[Warning], str, int]" +) -> None: + assert parse_warning_filter(arg, escape=escape) == expected + + +@pytest.mark.parametrize("arg", [":" * 5, "::::-1", "::::not-a-number"]) +def test_parse_warning_filter_failure(arg: str) -> None: + import warnings + + with pytest.raises(warnings._OptionError): + parse_warning_filter(arg, escape=True) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 550ebb4b8e7..b7a231094fd 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -240,9 +240,8 @@ def test_func(): def test_warning_captured_hook(testdir): testdir.makeconftest( """ - from _pytest.warnings import _issue_warning_captured def pytest_configure(config): - _issue_warning_captured(UserWarning("config warning"), config.hook, stacklevel=2) + config.issue_config_time_warning(UserWarning("config warning"), stacklevel=2) """ ) testdir.makepyfile( @@ -716,10 +715,22 @@ def test_issue4445_preparse(self, testdir, capwarn): assert "config{sep}__init__.py".format(sep=os.sep) in file assert func == "_preparse" + @pytest.mark.filterwarnings("default") + def test_conftest_warning_captured(self, testdir: Testdir) -> None: + """Warnings raised during importing of conftest.py files is captured (#2891).""" + testdir.makeconftest( + """ + import warnings + warnings.warn(UserWarning("my custom warning")) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + ["conftest.py:2", "*UserWarning: my custom warning*"] + ) + def test_issue4445_import_plugin(self, testdir, capwarn): - """#4445: Make sure the warning points to a reasonable location - See origin of _issue_warning_captured at: _pytest.config.__init__.py:585 - """ + """#4445: Make sure the warning points to a reasonable location""" testdir.makepyfile( some_plugin=""" import pytest @@ -738,7 +749,7 @@ def test_issue4445_import_plugin(self, testdir, capwarn): assert "skipped plugin 'some_plugin': thing" in str(warning.message) assert "config{sep}__init__.py".format(sep=os.sep) in file - assert func == "import_plugin" + assert func == "_warn_about_skipped_plugins" def test_issue4445_issue5928_mark_generator(self, testdir): """#4445 and #5928: Make sure the warning from an unknown mark points to From 3085c99e4768b6dc46a7a40c2e29216e12faeb84 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 25 Aug 2020 10:13:48 +0300 Subject: [PATCH 608/823] config: small doc improvements --- src/_pytest/config/__init__.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 5949c787bbc..ce88fc82f1f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -820,13 +820,13 @@ class Config: :param PytestPluginManager pluginmanager: :param InvocationParams invocation_params: - Object containing the parameters regarding the ``pytest.main`` + Object containing parameters regarding the :func:`pytest.main` invocation. """ @attr.s(frozen=True) class InvocationParams: - """Holds parameters passed during ``pytest.main()`` + """Holds parameters passed during :func:`pytest.main`. The object attributes are read-only. @@ -841,11 +841,20 @@ class InvocationParams: """ args = attr.ib(type=Tuple[str, ...], converter=_args_converter) - """Tuple of command-line arguments as passed to ``pytest.main()``.""" + """The command-line arguments as passed to :func:`pytest.main`. + + :type: Tuple[str, ...] + """ plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]]) - """List of extra plugins, might be `None`.""" + """Extra plugins, might be `None`. + + :type: Optional[Sequence[Union[str, plugin]]] + """ dir = attr.ib(type=Path) - """Directory from which ``pytest.main()`` was invoked.""" + """The directory from which :func:`pytest.main` was invoked. + + :type: pathlib.Path + """ def __init__( self, @@ -867,6 +876,10 @@ def __init__( """ self.invocation_params = invocation_params + """The parameters with which pytest was invoked. + + :type: InvocationParams + """ _a = FILE_OR_DIR self._parser = Parser( @@ -876,7 +889,7 @@ def __init__( self.pluginmanager = pluginmanager """The plugin manager handles plugin registration and hook invocation. - :type: PytestPluginManager. + :type: PytestPluginManager """ self.trace = self.pluginmanager.trace.root.get("config") @@ -901,7 +914,12 @@ def __init__( @property def invocation_dir(self) -> py.path.local: - """Backward compatibility.""" + """The directory from which pytest was invoked. + + Prefer to use :attr:`invocation_params.dir `. + + :type: py.path.local + """ return py.path.local(str(self.invocation_params.dir)) def add_cleanup(self, func: Callable[[], None]) -> None: From a346028006a7ca9d07788e837456b3ab3a1209eb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 6 Aug 2020 14:37:19 +0300 Subject: [PATCH 609/823] config: add Config.{rootpath,inipath}, turn Config.{rootdir,inifile} to properties --- changelog/7685.improvement.rst | 3 +++ doc/en/customize.rst | 11 +++++--- src/_pytest/config/__init__.py | 47 +++++++++++++++++++++++++++++++--- 3 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 changelog/7685.improvement.rst diff --git a/changelog/7685.improvement.rst b/changelog/7685.improvement.rst new file mode 100644 index 00000000000..59772162489 --- /dev/null +++ b/changelog/7685.improvement.rst @@ -0,0 +1,3 @@ +Added two new attributes :attr:`rootpath <_pytest.config.Config.rootpath>` and :attr:`inipath <_pytest.config.Config.inipath>` to :class:`Config <_pytest.config.Config>`. +These attributes are :class:`pathlib.Path` versions of the existing :attr:`rootdir <_pytest.config.Config.rootdir>` and :attr:`inifile <_pytest.config.Config.inifile>` attributes, +and should be preferred over them when possible. diff --git a/doc/en/customize.rst b/doc/en/customize.rst index e1f1b253bc9..9f7c365dc45 100644 --- a/doc/en/customize.rst +++ b/doc/en/customize.rst @@ -180,10 +180,15 @@ are never merged - the first match wins. The internal :class:`Config <_pytest.config.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture) will subsequently carry these attributes: -- ``config.rootdir``: the determined root directory, guaranteed to exist. +- :attr:`config.rootpath <_pytest.config.Config.rootpath>`: the determined root directory, guaranteed to exist. -- ``config.inifile``: the determined ``configfile``, may be ``None`` (it is named ``inifile`` - for historical reasons). +- :attr:`config.inipath <_pytest.config.Config.inipath>`: the determined ``configfile``, may be ``None`` + (it is named ``inipath`` for historical reasons). + +.. versionadded:: 6.1 + The ``config.rootpath`` and ``config.inipath`` properties. They are :class:`pathlib.Path` + versions of the older ``config.rootdir`` and ``config.inifile``, which have type + ``py.path.local``, and still exist for backward compatibility. The ``rootdir`` is used as a reference directory for constructing test addresses ("nodeids") and can be used also by plugins for storing diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ce88fc82f1f..6cda7da7106 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -916,12 +916,53 @@ def __init__( def invocation_dir(self) -> py.path.local: """The directory from which pytest was invoked. - Prefer to use :attr:`invocation_params.dir `. + Prefer to use :attr:`invocation_params.dir `, + which is a :class:`pathlib.Path`. :type: py.path.local """ return py.path.local(str(self.invocation_params.dir)) + @property + def rootpath(self) -> Path: + """The path to the :ref:`rootdir `. + + :type: pathlib.Path + + .. versionadded:: 6.1 + """ + return self._rootpath + + @property + def rootdir(self) -> py.path.local: + """The path to the :ref:`rootdir `. + + Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. + + :type: py.path.local + """ + return py.path.local(str(self.rootpath)) + + @property + def inipath(self) -> Optional[Path]: + """The path to the :ref:`configfile `. + + :type: Optional[pathlib.Path] + + .. versionadded:: 6.1 + """ + return self._inipath + + @property + def inifile(self) -> Optional[py.path.local]: + """The path to the :ref:`configfile `. + + Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. + + :type: Optional[py.path.local] + """ + return py.path.local(str(self.inipath)) if self.inipath else None + def add_cleanup(self, func: Callable[[], None]) -> None: """Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" @@ -1032,8 +1073,8 @@ def _initini(self, args: Sequence[str]) -> None: rootdir_cmd_arg=ns.rootdir or None, config=self, ) - self.rootdir = py.path.local(str(rootpath)) - self.inifile = py.path.local(str(inipath)) if inipath else None + self._rootpath = rootpath + self._inipath = inipath self.inicfg = inicfg self._parser.extra_info["rootdir"] = self.rootdir self._parser.extra_info["inifile"] = self.inifile From 62e249a1f934d1073c9a0167077e133c5e0f6270 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 6 Aug 2020 14:46:24 +0300 Subject: [PATCH 610/823] Replace some usages of config.{rootdir,inifile} with config.{rootpath,inipath} --- src/_pytest/cacheprovider.py | 6 +-- src/_pytest/config/__init__.py | 31 ++++++------ src/_pytest/fixtures.py | 12 +++-- src/_pytest/logging.py | 2 +- src/_pytest/main.py | 27 ++++++----- src/_pytest/nodes.py | 8 ++-- src/_pytest/pathlib.py | 3 +- src/_pytest/terminal.py | 36 +++++++------- testing/test_main.py | 88 ++++++++++++++++++---------------- testing/test_terminal.py | 3 +- 10 files changed, 117 insertions(+), 99 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index bc26c26bcd6..ba27735d039 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -78,7 +78,7 @@ def clear_cache(cls, cachedir: Path) -> None: @staticmethod def cache_dir_from_config(config: Config) -> Path: - return resolve_from_str(config.getini("cache_dir"), config.rootdir) + return resolve_from_str(config.getini("cache_dir"), config.rootpath) def warn(self, fmt: str, **args: object) -> None: import warnings @@ -264,7 +264,7 @@ def __init__(self, config: Config) -> None: def get_last_failed_paths(self) -> Set[Path]: """Return a set with all Paths()s of the previously failed nodeids.""" - rootpath = Path(str(self.config.rootdir)) + rootpath = self.config.rootpath result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed} return {x for x in result if x.exists()} @@ -495,7 +495,7 @@ def pytest_report_header(config: Config) -> Optional[str]: # starting with .., ../.. if sensible try: - displaypath = cachedir.relative_to(str(config.rootdir)) + displaypath = cachedir.relative_to(config.rootpath) except ValueError: displaypath = cachedir return "cachedir: {}".format(displaypath) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6cda7da7106..4b49a8467b9 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -47,6 +47,7 @@ from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import bestrelpath from _pytest.pathlib import import_path from _pytest.pathlib import ImportMode from _pytest.pathlib import Path @@ -520,7 +521,7 @@ def _getconftestmodules( else: directory = path - # XXX these days we may rather want to use config.rootdir + # XXX these days we may rather want to use config.rootpath # and allow users to opt into looking into the rootdir parent # directories instead of requiring to specify confcutdir. clist = [] @@ -1036,9 +1037,9 @@ def notify_exception( def cwd_relative_nodeid(self, nodeid: str) -> str: # nodeid's are relative to the rootpath, compute relative to cwd. - if self.invocation_dir != self.rootdir: - fullpath = self.rootdir.join(nodeid) - nodeid = self.invocation_dir.bestrelpath(fullpath) + if self.invocation_params.dir != self.rootpath: + fullpath = self.rootpath / nodeid + nodeid = bestrelpath(self.invocation_params.dir, fullpath) return nodeid @classmethod @@ -1076,8 +1077,8 @@ def _initini(self, args: Sequence[str]) -> None: self._rootpath = rootpath self._inipath = inipath self.inicfg = inicfg - self._parser.extra_info["rootdir"] = self.rootdir - self._parser.extra_info["inifile"] = self.inifile + self._parser.extra_info["rootdir"] = str(self.rootpath) + self._parser.extra_info["inifile"] = str(self.inipath) self._parser.addini("addopts", "extra command line options", "args") self._parser.addini("minversion", "minimally required pytest version") self._parser.addini( @@ -1169,8 +1170,8 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: self._validate_plugins() self._warn_about_skipped_plugins() - if self.known_args_namespace.confcutdir is None and self.inifile: - confcutdir = py.path.local(self.inifile).dirname + if self.known_args_namespace.confcutdir is None and self.inipath is not None: + confcutdir = str(self.inipath.parent) self.known_args_namespace.confcutdir = confcutdir try: self.hook.pytest_load_initial_conftests( @@ -1206,13 +1207,13 @@ def _checkversion(self) -> None: if not isinstance(minver, str): raise pytest.UsageError( - "%s: 'minversion' must be a single value" % self.inifile + "%s: 'minversion' must be a single value" % self.inipath ) if Version(minver) > Version(pytest.__version__): raise pytest.UsageError( "%s: 'minversion' requires pytest-%s, actual pytest-%s'" - % (self.inifile, minver, pytest.__version__,) + % (self.inipath, minver, pytest.__version__,) ) def _validate_config_options(self) -> None: @@ -1277,10 +1278,10 @@ def parse(self, args: List[str], addopts: bool = True) -> None: args, self.option, namespace=self.option ) if not args: - if self.invocation_dir == self.rootdir: + if self.invocation_params.dir == self.rootpath: args = self.getini("testpaths") if not args: - args = [str(self.invocation_dir)] + args = [str(self.invocation_params.dir)] self.args = args except PrintHelp: pass @@ -1383,10 +1384,10 @@ def _getini(self, name: str): # if type == "pathlist": # TODO: This assert is probably not valid in all cases. - assert self.inifile is not None - dp = py.path.local(self.inifile).dirpath() + assert self.inipath is not None + dp = self.inipath.parent input_values = shlex.split(value) if isinstance(value, str) else value - return [dp.join(x, abs=True) for x in input_values] + return [py.path.local(str(dp / x)) for x in input_values] elif type == "args": return shlex.split(value) if isinstance(value, str) else value elif type == "linelist": diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d2a08d57be0..bf77d09f119 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -50,6 +50,7 @@ from _pytest.mark import ParameterSet from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME +from _pytest.pathlib import absolutepath if TYPE_CHECKING: from typing import Deque @@ -1443,7 +1444,7 @@ def getfixtureinfo( def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: nodeid = None try: - p = py.path.local(plugin.__file__) # type: ignore[attr-defined] + p = absolutepath(plugin.__file__) # type: ignore[attr-defined] except AttributeError: pass else: @@ -1452,8 +1453,13 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: # Construct the base nodeid which is later used to check # what fixtures are visible for particular tests (as denoted # by their test id). - if p.basename.startswith("conftest.py"): - nodeid = p.dirpath().relto(self.config.rootdir) + if p.name.startswith("conftest.py"): + try: + nodeid = str(p.parent.relative_to(self.config.rootpath)) + except ValueError: + nodeid = "" + if nodeid == ".": + nodeid = "" if os.sep != nodes.SEP: nodeid = nodeid.replace(os.sep, nodes.SEP) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 95226e8cc3d..98386bacda0 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -603,7 +603,7 @@ def set_log_path(self, fname: str) -> None: fpath = Path(fname) if not fpath.is_absolute(): - fpath = Path(str(self._config.rootdir), fpath) + fpath = self._config.rootpath / fpath if not fpath.parent.exists(): fpath.parent.mkdir(exist_ok=True, parents=True) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 4ab91f82c07..4e35990adb3 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -33,6 +33,7 @@ from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath from _pytest.pathlib import Path from _pytest.pathlib import visit from _pytest.reports import CollectReport @@ -425,11 +426,11 @@ class Failed(Exception): @attr.s -class _bestrelpath_cache(Dict[py.path.local, str]): - path = attr.ib(type=py.path.local) +class _bestrelpath_cache(Dict[Path, str]): + path = attr.ib(type=Path) - def __missing__(self, path: py.path.local) -> str: - r = self.path.bestrelpath(path) # type: str + def __missing__(self, path: Path) -> str: + r = bestrelpath(self.path, path) self[path] = r return r @@ -444,8 +445,8 @@ class Session(nodes.FSCollector): exitstatus = None # type: Union[int, ExitCode] def __init__(self, config: Config) -> None: - nodes.FSCollector.__init__( - self, config.rootdir, parent=None, config=config, session=self, nodeid="" + super().__init__( + config.rootdir, parent=None, config=config, session=self, nodeid="" ) self.testsfailed = 0 self.testscollected = 0 @@ -456,8 +457,8 @@ def __init__(self, config: Config) -> None: self._initialpaths = frozenset() # type: FrozenSet[py.path.local] self._bestrelpathcache = _bestrelpath_cache( - config.rootdir - ) # type: Dict[py.path.local, str] + config.rootpath + ) # type: Dict[Path, str] self.config.pluginmanager.register(self, name="session") @@ -475,7 +476,7 @@ def __repr__(self) -> str: self.testscollected, ) - def _node_location_to_relpath(self, node_path: py.path.local) -> str: + def _node_location_to_relpath(self, node_path: Path) -> str: # bestrelpath is a quite slow function. return self._bestrelpathcache[node_path] @@ -599,7 +600,9 @@ def perform_collect( # noqa: F811 initialpaths = [] # type: List[py.path.local] for arg in args: fspath, parts = resolve_collection_argument( - self.config.invocation_dir, arg, as_pypath=self.config.option.pyargs + self.config.invocation_params.dir, + arg, + as_pypath=self.config.option.pyargs, ) self._initial_parts.append((fspath, parts)) initialpaths.append(fspath) @@ -817,7 +820,7 @@ def search_pypath(module_name: str) -> str: def resolve_collection_argument( - invocation_dir: py.path.local, arg: str, *, as_pypath: bool = False + invocation_path: Path, arg: str, *, as_pypath: bool = False ) -> Tuple[py.path.local, List[str]]: """Parse path arguments optionally containing selection parts and return (fspath, names). @@ -844,7 +847,7 @@ def resolve_collection_argument( strpath, *parts = str(arg).split("::") if as_pypath: strpath = search_pypath(strpath) - fspath = Path(str(invocation_dir), strpath) + fspath = invocation_path / strpath fspath = absolutepath(fspath) if not fspath.exists(): msg = ( diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 5dde4c7370e..3665d8d5ef4 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -31,6 +31,7 @@ from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords from _pytest.outcomes import fail +from _pytest.pathlib import absolutepath from _pytest.pathlib import Path from _pytest.store import Store @@ -401,7 +402,7 @@ def _repr_failure_py( # It will be better to just always display paths relative to invocation_dir, but # this requires a lot of plumbing (#6428). try: - abspath = Path(os.getcwd()) != Path(str(self.config.invocation_dir)) + abspath = Path(os.getcwd()) != self.config.invocation_params.dir except OSError: abspath = True @@ -597,10 +598,7 @@ def reportinfo(self) -> Tuple[Union[py.path.local, str], Optional[int], str]: @cached_property def location(self) -> Tuple[str, Optional[int], str]: location = self.reportinfo() - if isinstance(location[0], py.path.local): - fspath = location[0] - else: - fspath = py.path.local(location[0]) + fspath = absolutepath(str(location[0])) relfspath = self.session._node_location_to_relpath(fspath) assert type(location[2]) is str return (relfspath, location[1], location[2]) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 4a249c8fd27..355281039fd 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -366,8 +366,7 @@ def make_numbered_dir_with_cleanup( raise e -def resolve_from_str(input: str, root: py.path.local) -> Path: - rootpath = Path(root) +def resolve_from_str(input: str, rootpath: Path) -> Path: input = expanduser(input) input = expandvars(input) if isabs(input): diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index af68430000c..59d6aa97d03 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -40,6 +40,9 @@ from _pytest.config.argparsing import Parser from _pytest.nodes import Item from _pytest.nodes import Node +from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import Path from _pytest.reports import BaseReport from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -297,9 +300,9 @@ def get_location(self, config: Config) -> Optional[str]: if self.fslocation: if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2: filename, linenum = self.fslocation[:2] - relpath = py.path.local(filename).relto(config.invocation_dir) - if not relpath: - relpath = str(filename) + relpath = bestrelpath( + config.invocation_params.dir, absolutepath(filename) + ) return "{}:{}".format(relpath, linenum) else: return str(self.fslocation) @@ -319,11 +322,12 @@ def __init__(self, config: Config, file: Optional[TextIO] = None) -> None: self._main_color = None # type: Optional[str] self._known_types = None # type: Optional[List[str]] self.startdir = config.invocation_dir + self.startpath = config.invocation_params.dir if file is None: file = sys.stdout self._tw = _pytest.config.create_terminal_writer(config, file) self._screen_width = self._tw.fullwidth - self.currentfspath = None # type: Any + self.currentfspath = None # type: Union[None, Path, str, int] self.reportchars = getreportopt(config) self.hasmarkup = self._tw.hasmarkup self.isatty = file.isatty() @@ -385,19 +389,17 @@ def hasopt(self, char: str) -> bool: return char in self.reportchars def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None: - fspath = self.config.rootdir.join(nodeid.split("::")[0]) - # NOTE: explicitly check for None to work around py bug, and for less - # overhead in general (https://github.com/pytest-dev/py/pull/207). + fspath = self.config.rootpath / nodeid.split("::")[0] if self.currentfspath is None or fspath != self.currentfspath: if self.currentfspath is not None and self._show_progress_info: self._write_progress_information_filling_space() self.currentfspath = fspath - relfspath = self.startdir.bestrelpath(fspath) + relfspath = bestrelpath(self.startpath, fspath) self._tw.line() self._tw.write(relfspath + " ") self._tw.write(res, flush=True, **markup) - def write_ensure_prefix(self, prefix, extra: str = "", **kwargs) -> None: + def write_ensure_prefix(self, prefix: str, extra: str = "", **kwargs) -> None: if self.currentfspath != prefix: self._tw.line() self.currentfspath = prefix @@ -709,14 +711,14 @@ def _write_report_lines_from_hooks( self.write_line(line) def pytest_report_header(self, config: Config) -> List[str]: - line = "rootdir: %s" % config.rootdir + line = "rootdir: %s" % config.rootpath - if config.inifile: - line += ", configfile: " + config.rootdir.bestrelpath(config.inifile) + if config.inipath: + line += ", configfile: " + bestrelpath(config.rootpath, config.inipath) testpaths = config.getini("testpaths") if testpaths and config.args == testpaths: - rel_paths = [config.rootdir.bestrelpath(x) for x in testpaths] + rel_paths = [bestrelpath(config.rootpath, x) for x in testpaths] line += ", testpaths: {}".format(", ".join(rel_paths)) result = [line] @@ -860,7 +862,7 @@ def mkrel(nodeid): if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace( "\\", nodes.SEP ): - res += " <- " + self.startdir.bestrelpath(fspath) + res += " <- " + bestrelpath(self.startpath, fspath) else: res = "[location]" return res + " " @@ -1102,7 +1104,7 @@ def show_xpassed(lines: List[str]) -> None: def show_skipped(lines: List[str]) -> None: skipped = self.stats.get("skipped", []) # type: List[CollectReport] - fskips = _folded_skips(self.startdir, skipped) if skipped else [] + fskips = _folded_skips(self.startpath, skipped) if skipped else [] if not fskips: return verbose_word = skipped[0]._get_verbose_word(self.config) @@ -1230,7 +1232,7 @@ def _get_line_with_reprcrash_message( def _folded_skips( - startdir: py.path.local, skipped: Sequence[CollectReport], + startpath: Path, skipped: Sequence[CollectReport], ) -> List[Tuple[int, str, Optional[int], str]]: d = {} # type: Dict[Tuple[str, Optional[int], str], List[CollectReport]] for event in skipped: @@ -1239,7 +1241,7 @@ def _folded_skips( assert len(event.longrepr) == 3, (event, event.longrepr) fspath, lineno, reason = event.longrepr # For consistency, report all fspaths in relative form. - fspath = startdir.bestrelpath(py.path.local(fspath)) + fspath = bestrelpath(startpath, Path(fspath)) keywords = getattr(event, "keywords", {}) # Folding reports with global pytestmark variable. # This is a workaround, because for now we cannot identify the scope of a skip marker diff --git a/testing/test_main.py b/testing/test_main.py index 71eae16b0e5..5b45ec6b5bd 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -10,6 +10,7 @@ from _pytest.config import UsageError from _pytest.main import resolve_collection_argument from _pytest.main import validate_basetemp +from _pytest.pathlib import Path from _pytest.pytester import Testdir @@ -108,73 +109,79 @@ def test_validate_basetemp_integration(testdir): class TestResolveCollectionArgument: @pytest.fixture - def root(self, testdir): + def invocation_dir(self, testdir: Testdir) -> py.path.local: testdir.syspathinsert(str(testdir.tmpdir / "src")) testdir.chdir() pkg = testdir.tmpdir.join("src/pkg").ensure_dir() - pkg.join("__init__.py").ensure(file=True) - pkg.join("test.py").ensure(file=True) + pkg.join("__init__.py").ensure() + pkg.join("test.py").ensure() return testdir.tmpdir - def test_file(self, root): + @pytest.fixture + def invocation_path(self, invocation_dir: py.path.local) -> Path: + return Path(str(invocation_dir)) + + def test_file(self, invocation_dir: py.path.local, invocation_path: Path) -> None: """File and parts.""" - assert resolve_collection_argument(root, "src/pkg/test.py") == ( - root / "src/pkg/test.py", + assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == ( + invocation_dir / "src/pkg/test.py", [], ) - assert resolve_collection_argument(root, "src/pkg/test.py::") == ( - root / "src/pkg/test.py", + assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == ( + invocation_dir / "src/pkg/test.py", [""], ) - assert resolve_collection_argument(root, "src/pkg/test.py::foo::bar") == ( - root / "src/pkg/test.py", - ["foo", "bar"], - ) - assert resolve_collection_argument(root, "src/pkg/test.py::foo::bar::") == ( - root / "src/pkg/test.py", - ["foo", "bar", ""], - ) + assert resolve_collection_argument( + invocation_path, "src/pkg/test.py::foo::bar" + ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"]) + assert resolve_collection_argument( + invocation_path, "src/pkg/test.py::foo::bar::" + ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar", ""]) - def test_dir(self, root: py.path.local) -> None: + def test_dir(self, invocation_dir: py.path.local, invocation_path: Path) -> None: """Directory and parts.""" - assert resolve_collection_argument(root, "src/pkg") == (root / "src/pkg", []) + assert resolve_collection_argument(invocation_path, "src/pkg") == ( + invocation_dir / "src/pkg", + [], + ) with pytest.raises( UsageError, match=r"directory argument cannot contain :: selection parts" ): - resolve_collection_argument(root, "src/pkg::") + resolve_collection_argument(invocation_path, "src/pkg::") with pytest.raises( UsageError, match=r"directory argument cannot contain :: selection parts" ): - resolve_collection_argument(root, "src/pkg::foo::bar") + resolve_collection_argument(invocation_path, "src/pkg::foo::bar") - def test_pypath(self, root: py.path.local) -> None: + def test_pypath(self, invocation_dir: py.path.local, invocation_path: Path) -> None: """Dotted name and parts.""" - assert resolve_collection_argument(root, "pkg.test", as_pypath=True) == ( - root / "src/pkg/test.py", - [], - ) assert resolve_collection_argument( - root, "pkg.test::foo::bar", as_pypath=True - ) == (root / "src/pkg/test.py", ["foo", "bar"],) - assert resolve_collection_argument(root, "pkg", as_pypath=True) == ( - root / "src/pkg", + invocation_path, "pkg.test", as_pypath=True + ) == (invocation_dir / "src/pkg/test.py", []) + assert resolve_collection_argument( + invocation_path, "pkg.test::foo::bar", as_pypath=True + ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"]) + assert resolve_collection_argument(invocation_path, "pkg", as_pypath=True) == ( + invocation_dir / "src/pkg", [], ) with pytest.raises( UsageError, match=r"package argument cannot contain :: selection parts" ): - resolve_collection_argument(root, "pkg::foo::bar", as_pypath=True) + resolve_collection_argument( + invocation_path, "pkg::foo::bar", as_pypath=True + ) - def test_does_not_exist(self, root): + def test_does_not_exist(self, invocation_path: Path) -> None: """Given a file/module that does not exist raises UsageError.""" with pytest.raises( UsageError, match=re.escape("file or directory not found: foobar") ): - resolve_collection_argument(root, "foobar") + resolve_collection_argument(invocation_path, "foobar") with pytest.raises( UsageError, @@ -182,12 +189,14 @@ def test_does_not_exist(self, root): "module or package not found: foobar (missing __init__.py?)" ), ): - resolve_collection_argument(root, "foobar", as_pypath=True) + resolve_collection_argument(invocation_path, "foobar", as_pypath=True) - def test_absolute_paths_are_resolved_correctly(self, root): + def test_absolute_paths_are_resolved_correctly( + self, invocation_dir: py.path.local, invocation_path: Path + ) -> None: """Absolute paths resolve back to absolute paths.""" - full_path = str(root / "src") - assert resolve_collection_argument(root, full_path) == ( + full_path = str(invocation_dir / "src") + assert resolve_collection_argument(invocation_path, full_path) == ( py.path.local(os.path.abspath("src")), [], ) @@ -195,10 +204,9 @@ def test_absolute_paths_are_resolved_correctly(self, root): # ensure full paths given in the command-line without the drive letter resolve # to the full path correctly (#7628) drive, full_path_without_drive = os.path.splitdrive(full_path) - assert resolve_collection_argument(root, full_path_without_drive) == ( - py.path.local(os.path.abspath("src")), - [], - ) + assert resolve_collection_argument( + invocation_path, full_path_without_drive + ) == (py.path.local(os.path.abspath("src")), []) def test_module_full_path_without_drive(testdir): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 0e26ae13c78..57db1b9a529 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -18,6 +18,7 @@ from _pytest._io.wcwidth import wcswidth from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.pathlib import Path from _pytest.pytester import Testdir from _pytest.reports import BaseReport from _pytest.reports import CollectReport @@ -2085,7 +2086,7 @@ class X: ev3.longrepr = longrepr ev3.skipped = True - values = _folded_skips(py.path.local(), [ev1, ev2, ev3]) + values = _folded_skips(Path.cwd(), [ev1, ev2, ev3]) assert len(values) == 1 num, fspath, lineno_, reason = values[0] assert num == 3 From 48a8c373a03ad3581243b218c4a8b560fbb12ff1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 4 Sep 2020 20:14:39 +0300 Subject: [PATCH 611/823] doc/reference: include PluginManager in PytestPluginManager and remove it Before, `PluginManager` was a copy of the pluggy doc, and `PytestPluginManager` was documented separately. pytest users only really need to know about `PytestPluginManager`, so instead of splitting have the `PytestPluginManager` documentation include all of `PluginManager` members and remove `PluginManager` from the reference (it is still shown as the base class). --- doc/en/reference.rst | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 313c76c4c86..1816c467e47 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -872,12 +872,6 @@ Parser .. autoclass:: _pytest.config.argparsing.Parser() :members: -PluginManager -~~~~~~~~~~~~~ - -.. autoclass:: pluggy.PluginManager() - :members: - PytestPluginManager ~~~~~~~~~~~~~~~~~~~ @@ -885,6 +879,7 @@ PytestPluginManager .. autoclass:: _pytest.config.PytestPluginManager() :members: :undoc-members: + :inherited-members: :show-inheritance: Session From 0ca23270699584b2b4cb0d83acc041a2cde22126 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 4 Sep 2020 20:46:15 +0300 Subject: [PATCH 612/823] doc: prefer to reference by public name when possible When a name is exported from `pytest`, prefer to refer to it by that rather than its `_pytest` import path. It is shorter and more appropriate in user-facing documentation (although that's not really visible). Our plan is to expose more names for typing purposes, in which can this could be more comprehensive. --- doc/en/fixture.rst | 8 ++++---- doc/en/reference.rst | 22 +++++++++++----------- doc/en/usage.rst | 2 +- src/_pytest/hookspec.py | 16 ++++++++-------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index dd1f416cbdb..e0648a6a288 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -121,7 +121,7 @@ Fixtures as Function arguments Test functions can receive fixture objects by naming them as an input argument. For each argument name, a fixture function with that name provides the fixture object. Fixture functions are registered by marking them with -:py:func:`@pytest.fixture <_pytest.python.fixture>`. Let's look at a simple +:py:func:`@pytest.fixture `. Let's look at a simple self-contained test module containing a fixture and a test function using it: @@ -144,7 +144,7 @@ using it: assert 0 # for demo purposes Here, the ``test_ehlo`` needs the ``smtp_connection`` fixture value. pytest -will discover and call the :py:func:`@pytest.fixture <_pytest.python.fixture>` +will discover and call the :py:func:`@pytest.fixture ` marked ``smtp_connection`` fixture function. Running the test looks like this: .. code-block:: pytest @@ -252,7 +252,7 @@ Scope: sharing fixtures across classes, modules, packages or session Fixtures requiring network access depend on connectivity and are usually time-expensive to create. Extending the previous example, we can add a ``scope="module"`` parameter to the -:py:func:`@pytest.fixture <_pytest.python.fixture>` invocation +:py:func:`@pytest.fixture ` invocation to cause the decorated ``smtp_connection`` fixture function to only be invoked once per test *module* (the default is to invoke once per test *function*). Multiple test functions in a test module will thus @@ -775,7 +775,7 @@ through the special :py:class:`request ` object: smtp_connection.close() The main change is the declaration of ``params`` with -:py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values +:py:func:`@pytest.fixture `, a list of values for each of which the fixture function will execute and can access a value via ``request.param``. No test function code needs to change. So let's just do another run: diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 313c76c4c86..700d10133bd 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -240,7 +240,7 @@ For example: ... Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to the collected -:class:`Item <_pytest.nodes.Item>`, which can then be accessed by fixtures or hooks with +:class:`Item `, which can then be accessed by fixtures or hooks with :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`. The ``mark`` object will have the following attributes: .. code-block:: python @@ -676,7 +676,7 @@ items, delete or otherwise amend the test items: Test running (runtest) hooks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -All runtest related hooks receive a :py:class:`pytest.Item <_pytest.main.Item>` object. +All runtest related hooks receive a :py:class:`pytest.Item ` object. .. autofunction:: pytest_runtestloop .. autofunction:: pytest_runtest_protocol @@ -752,14 +752,14 @@ CallInfo Class ~~~~~ -.. autoclass:: _pytest.python.Class() +.. autoclass:: pytest.Class() :members: :show-inheritance: Collector ~~~~~~~~~ -.. autoclass:: _pytest.nodes.Collector() +.. autoclass:: pytest.Collector() :members: :show-inheritance: @@ -787,13 +787,13 @@ ExceptionInfo ExitCode ~~~~~~~~ -.. autoclass:: _pytest.config.ExitCode +.. autoclass:: pytest.ExitCode :members: File ~~~~ -.. autoclass:: _pytest.nodes.File() +.. autoclass:: pytest.File() :members: :show-inheritance: @@ -815,14 +815,14 @@ FSCollector Function ~~~~~~~~ -.. autoclass:: _pytest.python.Function() +.. autoclass:: pytest.Function() :members: :show-inheritance: Item ~~~~ -.. autoclass:: _pytest.nodes.Item() +.. autoclass:: pytest.Item() :members: :show-inheritance: @@ -856,7 +856,7 @@ Metafunc Module ~~~~~~ -.. autoclass:: _pytest.python.Module() +.. autoclass:: pytest.Module() :members: :show-inheritance: @@ -890,7 +890,7 @@ PytestPluginManager Session ~~~~~~~ -.. autoclass:: _pytest.main.Session() +.. autoclass:: pytest.Session() :members: :show-inheritance: @@ -1032,7 +1032,7 @@ When set (regardless of value), pytest will use color in terminal output. Exceptions ---------- -.. autoclass:: _pytest.config.UsageError() +.. autoclass:: pytest.UsageError() :show-inheritance: .. _`warnings ref`: diff --git a/doc/en/usage.rst b/doc/en/usage.rst index aafdeb55f45..6b2d529f860 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -33,7 +33,7 @@ Running ``pytest`` can result in six different exit codes: :Exit code 4: pytest command line usage error :Exit code 5: No tests were collected -They are represented by the :class:`_pytest.config.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using: +They are represented by the :class:`pytest.ExitCode` enum. The exit codes being a part of the public API can be imported and accessed directly using: .. code-block:: python diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index d4aaba1ec23..2f0a04a06a7 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -237,7 +237,7 @@ def pytest_collection(session: "Session") -> Optional[object]: for example the terminal plugin uses it to start displaying the collection counter (and returns `None`). - :param _pytest.main.Session session: The pytest session object. + :param pytest.Session session: The pytest session object. """ @@ -247,16 +247,16 @@ def pytest_collection_modifyitems( """Called after collection has been performed. May filter or re-order the items in-place. - :param _pytest.main.Session session: The pytest session object. + :param pytest.Session session: The pytest session object. :param _pytest.config.Config config: The pytest config object. - :param List[_pytest.nodes.Item] items: List of item objects. + :param List[pytest.Item] items: List of item objects. """ def pytest_collection_finish(session: "Session") -> None: """Called after collection has been performed and modified. - :param _pytest.main.Session session: The pytest session object. + :param pytest.Session session: The pytest session object. """ @@ -393,7 +393,7 @@ def pytest_runtestloop(session: "Session") -> Optional[object]: If at any point ``session.shouldfail`` or ``session.shouldstop`` are set, the loop is terminated after the runtest protocol for the current item is finished. - :param _pytest.main.Session session: The pytest session object. + :param pytest.Session session: The pytest session object. Stops at first non-None result, see :ref:`firstresult`. The return value is not used, but only stops further processing. @@ -572,7 +572,7 @@ def pytest_sessionstart(session: "Session") -> None: """Called after the ``Session`` object has been created and before performing collection and entering the run test loop. - :param _pytest.main.Session session: The pytest session object. + :param pytest.Session session: The pytest session object. """ @@ -581,7 +581,7 @@ def pytest_sessionfinish( ) -> None: """Called after whole test run finished, right before returning the exit status to the system. - :param _pytest.main.Session session: The pytest session object. + :param pytest.Session session: The pytest session object. :param int exitstatus: The status which pytest will return to the system. """ @@ -633,7 +633,7 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No You need to **clean the .pyc** files in your project directory and interpreter libraries when enabling this option, as assertions will require to be re-written. - :param _pytest.nodes.Item item: pytest item object of current test. + :param pytest.Item item: pytest item object of current test. :param int lineno: Line number of the assert statement. :param str orig: String with the original assertion. :param str expl: String with the assert explanation. From 5371be4cf6858b7bdf523b450864c167f0c8d0c4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 4 Sep 2020 17:03:47 -0300 Subject: [PATCH 613/823] Use tox to execute release script The release-on-comment script is always executed on *master*, so we should execute the `release.py` script using tox to ensure we create the right environment. Also fixed errors in the error handling code. --- scripts/release-on-comment.py | 67 +++++++++++++++-------------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/scripts/release-on-comment.py b/scripts/release-on-comment.py index ae727e3dee3..7c662113e06 100644 --- a/scripts/release-on-comment.py +++ b/scripts/release-on-comment.py @@ -2,8 +2,8 @@ This script is part of the pytest release process which is triggered by comments in issues. -This script is started by the `release-on-comment.yml` workflow, which is triggered by two comment -related events: +This script is started by the `release-on-comment.yml` workflow, which always executes on +`master` and is triggered by two comment related events: * https://help.github.com/en/actions/reference/events-that-trigger-workflows#issue-comment-event-issue_comment * https://help.github.com/en/actions/reference/events-that-trigger-workflows#issues-event-issues @@ -30,7 +30,7 @@ import json import os import re -import sys +import traceback from pathlib import Path from subprocess import CalledProcessError from subprocess import check_call @@ -94,7 +94,6 @@ def print_and_exit(msg) -> None: def trigger_release(payload_path: Path, token: str) -> None: - error_contents = "" # to be used to store error output in case any command fails payload, base_branch, is_major = validate_and_get_issue_comment_payload( payload_path ) @@ -119,6 +118,7 @@ def trigger_release(payload_path: Path, token: str) -> None: issue.create_comment(str(e)) print_and_exit(f"{Fore.RED}{e}") + error_contents = "" try: print(f"Version: {Fore.CYAN}{version}") @@ -146,11 +146,12 @@ def trigger_release(payload_path: Path, token: str) -> None: print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.") + # important to use tox here because we have changed branches, so dependencies + # might have changed as well + cmdline = ["tox", "-e", "release", "--", version, "--skip-check-links"] + print("Running", " ".join(cmdline)) run( - [sys.executable, "scripts/release.py", version, "--skip-check-links"], - text=True, - check=True, - capture_output=True, + cmdline, text=True, check=True, capture_output=True, ) oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git" @@ -178,43 +179,31 @@ def trigger_release(payload_path: Path, token: str) -> None: ) print(f"Notified in original comment {Fore.CYAN}{comment.url}{Fore.RESET}.") - print(f"{Fore.GREEN}Success.") except CalledProcessError as e: - error_contents = e.output - except Exception as e: - error_contents = str(e) - link = f"https://github.com/{SLUG}/actions/runs/{os.environ['GITHUB_RUN_ID']}" - issue.create_comment( - dedent( - f""" - Sorry, the request to prepare release `{version}` from {base_branch} failed with: - - ``` - {e} - ``` - - See: {link}. - """ - ) - ) - print_and_exit(f"{Fore.RED}{e}") + error_contents = f"CalledProcessError\noutput:\n{e.output}\nstderr:\n{e.stderr}" + except Exception: + error_contents = f"Exception:\n{traceback.format_exc()}" if error_contents: link = f"https://github.com/{SLUG}/actions/runs/{os.environ['GITHUB_RUN_ID']}" - issue.create_comment( - dedent( - f""" - Sorry, the request to prepare release `{version}` from {base_branch} failed with: - - ``` - {error_contents} - ``` - - See: {link}. - """ - ) + msg = ERROR_COMMENT.format( + version=version, base_branch=base_branch, contents=error_contents, link=link ) + issue.create_comment(msg) print_and_exit(f"{Fore.RED}{error_contents}") + else: + print(f"{Fore.GREEN}Success.") + + +ERROR_COMMENT = """\ +The request to prepare release `{version}` from {base_branch} failed with: + +``` +{contents} +``` + +See: {link}. +""" def find_next_version(base_branch: str, is_major: bool) -> str: From 1df2471f17bfad3858e7daa13221a2bde34fa8c2 Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Sat, 5 Sep 2020 13:18:29 +0100 Subject: [PATCH 614/823] Make min duration configurable for slowest tests (#7667) Co-authored-by: Bruno Oliveira --- changelog/7667.feature.rst | 1 + doc/en/usage.rst | 7 ++++--- src/_pytest/runner.py | 15 ++++++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 changelog/7667.feature.rst diff --git a/changelog/7667.feature.rst b/changelog/7667.feature.rst new file mode 100644 index 00000000000..3928495e51f --- /dev/null +++ b/changelog/7667.feature.rst @@ -0,0 +1 @@ +New ``--durations-min`` command-line flag controls the minimal duration for inclusion in the slowest list of tests shown by ``--durations``. Previously this was hard-coded to ``0.005s``. diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 6b2d529f860..1c001060e89 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -426,14 +426,15 @@ Pytest supports the use of ``breakpoint()`` with the following behaviours: Profiling test execution duration ------------------------------------- +.. versionchanged:: 6.0 -To get a list of the slowest 10 test durations: +To get a list of the slowest 10 test durations over 1.0s long: .. code-block:: bash - pytest --durations=10 + pytest --durations=10 --durations-min=1.0 -By default, pytest will not show test durations that are too small (<0.01s) unless ``-vv`` is passed on the command-line. +By default, pytest will not show test durations that are too small (<0.005s) unless ``-vv`` is passed on the command-line. .. _faulthandler: diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 4089fc689fd..2dc940b395e 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -52,10 +52,19 @@ def pytest_addoption(parser: Parser) -> None: metavar="N", help="show N slowest setup/test durations (N=0 for all).", ) + group.addoption( + "--durations-min", + action="store", + type=float, + default=0.005, + metavar="N", + help="Minimal duration in seconds for inclusion in slowest list. Default 0.005", + ) def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: durations = terminalreporter.config.option.durations + durations_min = terminalreporter.config.option.durations_min verbose = terminalreporter.config.getvalue("verbose") if durations is None: return @@ -76,11 +85,11 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: dlist = dlist[:durations] for i, rep in enumerate(dlist): - if verbose < 2 and rep.duration < 0.005: + if verbose < 2 and rep.duration < durations_min: tr.write_line("") tr.write_line( - "(%s durations < 0.005s hidden. Use -vv to show these durations.)" - % (len(dlist) - i) + "(%s durations < %gs hidden. Use -vv to show these durations.)" + % (len(dlist) - i, durations_min) ) break tr.write_line("{:02.2f}s {:<8} {}".format(rep.duration, rep.when, rep.nodeid)) From 5bdfd719e644cf146843939d81aa2869422d012d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 5 Sep 2020 18:11:23 +0200 Subject: [PATCH 615/823] doc: Remove Workshoptage training The sign-up is closed. --- doc/en/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index a9bc07fbd0c..ecfeb753611 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -2,7 +2,6 @@ .. sidebar:: Next Open Trainings - - `pytest: Test Driven Development (nicht nur) für Python `_ (German) at the `CH Open Workshoptage `_, September 8 2020, HSLU Campus Rotkreuz (ZG), Switzerland. - `Professional testing with Python `_, via Python Academy, February 1-3 2021, Leipzig (Germany) and remote. Also see `previous talks and blogposts `_. From e503c9a9f80ed2bd41e8ab643261bdb83607dc08 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 6 Sep 2020 11:52:30 +0300 Subject: [PATCH 616/823] doc: fix broken cross references --- doc/en/cache.rst | 2 +- doc/en/deprecations.rst | 7 +++---- doc/en/fixture.rst | 4 ++-- doc/en/funcarg_compare.rst | 4 ++-- doc/en/historical-notes.rst | 2 +- doc/en/monkeypatch.rst | 26 +++++++++++++------------- doc/en/reference.rst | 9 +++------ doc/en/warnings.rst | 2 +- src/_pytest/tmpdir.py | 2 +- 9 files changed, 27 insertions(+), 31 deletions(-) diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 076d4b1973c..42ca473545d 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -264,7 +264,7 @@ the cache and nothing will be printed: FAILED test_caching.py::test_function - assert 42 == 23 1 failed in 0.12s -See the :fixture:`config.cache fixture ` for more details. +See the :fixture:`config.cache fixture ` for more details. Inspecting Cache content diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 9f15553c740..14d1eeb98af 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -16,8 +16,7 @@ Deprecated Features ------------------- Below is a complete list of all pytest features which are considered deprecated. Using those features will issue -:class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using -:ref:`standard warning filters `. +:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. The ``pytest_warning_captured`` hook @@ -376,7 +375,7 @@ Metafunc.addcall .. versionremoved:: 4.0 -:meth:`_pytest.python.Metafunc.addcall` was a precursor to the current parametrized mechanism. Users should use +``_pytest.python.Metafunc.addcall`` was a precursor to the current parametrized mechanism. Users should use :meth:`_pytest.python.Metafunc.parametrize` instead. Example: @@ -611,7 +610,7 @@ This has been documented as deprecated for years, but only now we are actually e .. versionremoved:: 4.0 -As part of a large :ref:`marker-revamp`, :meth:`_pytest.nodes.Node.get_marker` is deprecated. See +As part of a large :ref:`marker-revamp`, ``_pytest.nodes.Node.get_marker`` is removed. See :ref:`the documentation ` on tips on how to update your code. diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index e0648a6a288..b05f60876a2 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -592,7 +592,7 @@ will not be executed. Fixtures can introspect the requesting test context ------------------------------------------------------------- -Fixture functions can accept the :py:class:`request ` object +Fixture functions can accept the :py:class:`request <_pytest.fixtures.FixtureRequest>` object to introspect the "requesting" test function, class or module context. Further extending the previous ``smtp_connection`` fixture example, let's read an optional server URL from the test module which uses our fixture: @@ -664,7 +664,7 @@ from the module namespace. Using markers to pass data to fixtures ------------------------------------------------------------- -Using the :py:class:`request ` object, a fixture can also access +Using the :py:class:`request <_pytest.fixtures.FixtureRequest>` object, a fixture can also access markers which are applied to a test function. This can be useful to pass data into a fixture from a test: diff --git a/doc/en/funcarg_compare.rst b/doc/en/funcarg_compare.rst index 33b19ab0f35..0c4913edff8 100644 --- a/doc/en/funcarg_compare.rst +++ b/doc/en/funcarg_compare.rst @@ -51,7 +51,7 @@ There are several limitations and difficulties with this approach: performs parametrization at the places where the resource is used. Moreover, you need to modify the factory to use an ``extrakey`` parameter containing ``request.param`` to the - :py:func:`~python.Request.cached_setup` call. + ``Request.cached_setup`` call. 3. Multiple parametrized session-scoped resources will be active at the same time, making it hard for them to affect global state @@ -113,7 +113,7 @@ This new way of parametrizing funcarg factories should in many cases allow to re-use already written factories because effectively ``request.param`` was already used when test functions/classes were parametrized via -:py:func:`~_pytest.python.Metafunc.parametrize(indirect=True)` calls. +:py:func:`metafunc.parametrize(indirect=True) <_pytest.python.Metafunc.parametrize>` calls. Of course it's perfectly fine to combine parametrization and scoping: diff --git a/doc/en/historical-notes.rst b/doc/en/historical-notes.rst index ba96d32ab87..4f8722c1c16 100644 --- a/doc/en/historical-notes.rst +++ b/doc/en/historical-notes.rst @@ -112,7 +112,7 @@ More details can be found in the `original PR ` to patch the function or property with your desired testing behavior. This can include your own functions. -Use :py:meth:`monkeypatch.delattr` to remove the function or property for the test. +Use :py:meth:`monkeypatch.delattr ` to remove the function or property for the test. 2. Modifying the values of dictionaries e.g. you have a global configuration that -you want to modify for certain test cases. Use :py:meth:`monkeypatch.setitem` to patch the -dictionary for the test. :py:meth:`monkeypatch.delitem` can be used to remove items. +you want to modify for certain test cases. Use :py:meth:`monkeypatch.setitem ` to patch the +dictionary for the test. :py:meth:`monkeypatch.delitem ` can be used to remove items. 3. Modifying environment variables for a test e.g. to test program behavior if an environment variable is missing, or to set multiple values to a known variable. -:py:meth:`monkeypatch.setenv` and :py:meth:`monkeypatch.delenv` can be used for +:py:meth:`monkeypatch.setenv ` and :py:meth:`monkeypatch.delenv ` can be used for these patches. 4. Use ``monkeypatch.setenv("PATH", value, prepend=os.pathsep)`` to modify ``$PATH``, and -:py:meth:`monkeypatch.chdir` to change the context of the current working directory +:py:meth:`monkeypatch.chdir ` to change the context of the current working directory during a test. -5. Use :py:meth:`monkeypatch.syspath_prepend` to modify ``sys.path`` which will also -call :py:meth:`pkg_resources.fixup_namespace_packages` and :py:meth:`importlib.invalidate_caches`. +5. Use :py:meth:`monkeypatch.syspath_prepend ` to modify ``sys.path`` which will also +call ``pkg_resources.fixup_namespace_packages`` and :py:func:`importlib.invalidate_caches`. See the `monkeypatch blog post`_ for some introduction material and a discussion of its motivation. @@ -66,10 +66,10 @@ testing, you do not want your test to depend on the running user. ``monkeypatch` can be used to patch functions dependent on the user to always return a specific value. -In this example, :py:meth:`monkeypatch.setattr` is used to patch ``Path.home`` +In this example, :py:meth:`monkeypatch.setattr ` is used to patch ``Path.home`` so that the known testing path ``Path("/abc")`` is always used when the test is run. This removes any dependency on the running user for testing purposes. -:py:meth:`monkeypatch.setattr` must be called before the function which will use +:py:meth:`monkeypatch.setattr ` must be called before the function which will use the patched function is called. After the test function finishes the ``Path.home`` modification will be undone. @@ -102,7 +102,7 @@ After the test function finishes the ``Path.home`` modification will be undone. Monkeypatching returned objects: building mock classes ------------------------------------------------------ -:py:meth:`monkeypatch.setattr` can be used in conjunction with classes to mock returned +:py:meth:`monkeypatch.setattr ` can be used in conjunction with classes to mock returned objects from functions instead of values. Imagine a simple function to take an API url and return the json response. @@ -330,7 +330,7 @@ This behavior can be moved into ``fixture`` structures and shared across tests: Monkeypatching dictionaries --------------------------- -:py:meth:`monkeypatch.setitem` can be used to safely set the values of dictionaries +:py:meth:`monkeypatch.setitem ` can be used to safely set the values of dictionaries to specific values during tests. Take this simplified connection string example: .. code-block:: python @@ -367,7 +367,7 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific result = app.create_connection_string() assert result == expected -You can use the :py:meth:`monkeypatch.delitem` to remove values. +You can use the :py:meth:`monkeypatch.delitem ` to remove values. .. code-block:: python diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 24dc21de44c..bda5fe6dc22 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -539,14 +539,11 @@ recwarn .. autofunction:: recwarn() :no-auto-options: -.. autoclass:: _pytest.recwarn.WarningsRecorder() +.. autoclass:: WarningsRecorder() :members: Each recorded warning is an instance of :class:`warnings.WarningMessage`. -.. note:: - :class:`RecordedWarning` was changed from a plain class to a namedtuple in pytest 3.1 - .. note:: ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated differently; see :ref:`ensuring_function_triggers`. @@ -688,8 +685,8 @@ All runtest related hooks receive a :py:class:`pytest.Item ` object .. autofunction:: pytest_runtest_makereport For deeper understanding you may look at the default implementation of -these hooks in :py:mod:`_pytest.runner` and maybe also -in :py:mod:`_pytest.pdb` which interacts with :py:mod:`_pytest.capture` +these hooks in ``_pytest.runner`` and maybe also +in ``_pytest.pdb`` which interacts with ``_pytest.capture`` and its input/output capturing in order to immediately drop into interactive debugging when a test failure occurs. diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index fe2ef39dce2..7232b676d24 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -367,7 +367,7 @@ warnings, or index into it to get a particular recorded warning. .. currentmodule:: _pytest.warnings -Full API: :class:`WarningsRecorder`. +Full API: :class:`~_pytest.recwarn.WarningsRecorder`. .. _custom_failure_messages: diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index f1a8f1e9f11..7eb19b59e57 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -111,7 +111,7 @@ class TempdirFactory: _tmppath_factory = attr.ib(type=TempPathFactory) def mktemp(self, basename: str, numbered: bool = True) -> py.path.local: - """Same as :meth:`TempPathFactory.mkdir`, but returns a ``py.path.local`` object.""" + """Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object.""" return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve()) def getbasetemp(self) -> py.path.local: From 7da6ebede08c6473fcadf589dccd6d59d56fa22e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 8 Sep 2020 09:52:59 -0700 Subject: [PATCH 617/823] Update pytest-dev requirements - be more vague about "packaging metadata" over explicitly mentioning `setup.py` (such that `pyproject.toml`-based distributions are allowed) - drop extensions on `README.txt` / `LICENSE.txt` (it's more common to have `.md` / `.rst` / no extension) - remove duplicate mention of license packaging metadata --- CONTRIBUTING.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 4481fee3818..50bd0e5e71f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -148,16 +148,16 @@ You can submit your plugin by subscribing to the `pytest-dev mail list mail pointing to your existing pytest plugin repository which must have the following: -- PyPI presence with a ``setup.py`` that contains a license, ``pytest-`` +- PyPI presence with packaging metadata that contains a ``pytest-`` prefixed name, version number, authors, short and long description. - a ``tox.ini`` for running tests using `tox `_. -- a ``README.txt`` describing how to use the plugin and on which +- a ``README`` describing how to use the plugin and on which platforms it runs. -- a ``LICENSE.txt`` file or equivalent containing the licensing - information, with matching info in ``setup.py``. +- a ``LICENSE`` file containing the licensing information, with + matching info in its packaging metadata. - an issue tracker for bug reports and enhancement requests. From 6ae0f741ef646267fdebfa05dcc1c44c66e5b9fc Mon Sep 17 00:00:00 2001 From: Joseph Lucas Date: Wed, 9 Sep 2020 09:33:39 -0400 Subject: [PATCH 618/823] Add example for __test__ (#7733) Co-authored-by: Bruno Oliveira --- doc/en/example/pythoncollection.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index a12e2deaa77..c2f0348395c 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -313,3 +313,12 @@ interpreter: collect_ignore = ["setup.py"] if sys.version_info[0] > 2: collect_ignore_glob = ["*_py2.py"] + +Since Pytest 2.6, users can prevent pytest from discovering classes that start +with ``Test`` by setting a boolean ``__test__`` attribute to ``False``. + +.. code-block:: python + + # Will not be discovered as a test + class TestClass: + __test__ = False From c00fe960baa40b5a4b460988d2d1202a8e3d668b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 10 Sep 2020 12:38:43 -0300 Subject: [PATCH 619/823] Allow ovewriting a parametrized fixture while reusing the parent fixture's value Fix #1953 --- changelog/1953.bugfix.rst | 20 ++++++ src/_pytest/fixtures.py | 75 ++++++++++++++-------- testing/python/fixtures.py | 126 +++++++++++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+), 26 deletions(-) create mode 100644 changelog/1953.bugfix.rst diff --git a/changelog/1953.bugfix.rst b/changelog/1953.bugfix.rst new file mode 100644 index 00000000000..9db33ab10f5 --- /dev/null +++ b/changelog/1953.bugfix.rst @@ -0,0 +1,20 @@ +Fix error when overwriting a parametrized fixture, while also reusing the super fixture value. + +.. code-block:: python + + # conftest.py + import pytest + + + @pytest.fixture(params=[1, 2]) + def foo(request): + return request.param + + + # test_foo.py + import pytest + + + @pytest.fixture + def foo(foo): + return foo * 2 diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index bf77d09f119..f0e02b8b9d4 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -47,6 +47,7 @@ from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.deprecated import FILLFUNCARGS +from _pytest.mark import Mark from _pytest.mark import ParameterSet from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -1529,34 +1530,56 @@ def sort_by_scope(arg_name: str) -> int: return initialnames, fixturenames_closure, arg2fixturedefs def pytest_generate_tests(self, metafunc: "Metafunc") -> None: + """Generate new tests based on parametrized fixtures used by the given metafunc""" + + def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]: + if "argnames" in mark.kwargs: + argnames = mark.kwargs[ + "argnames" + ] # type: Union[str, Tuple[str, ...], List[str]] + else: + argnames = mark.args[0] + if not isinstance(argnames, (tuple, list)): + argnames = [x.strip() for x in argnames.split(",") if x.strip()] + return argnames + for argname in metafunc.fixturenames: - faclist = metafunc._arg2fixturedefs.get(argname) - if faclist: - fixturedef = faclist[-1] + # Get the FixtureDefs for the argname. + fixture_defs = metafunc._arg2fixturedefs.get(argname) + if not fixture_defs: + # Will raise FixtureLookupError at setup time if not parametrized somewhere + # else (e.g @pytest.mark.parametrize) + continue + + # If the test itself parametrizes using this argname, give it + # precedence. + if any( + argname in get_parametrize_mark_argnames(mark) + for mark in metafunc.definition.iter_markers("parametrize") + ): + continue + + # In the common case we only look at the fixture def with the + # closest scope (last in the list). But if the fixture overrides + # another fixture, while requesting the super fixture, keep going + # in case the super fixture is parametrized (#1953). + for fixturedef in reversed(fixture_defs): + # Fixture is parametrized, apply it and stop. if fixturedef.params is not None: - markers = list(metafunc.definition.iter_markers("parametrize")) - for parametrize_mark in markers: - if "argnames" in parametrize_mark.kwargs: - argnames = parametrize_mark.kwargs["argnames"] - else: - argnames = parametrize_mark.args[0] - - if not isinstance(argnames, (tuple, list)): - argnames = [ - x.strip() for x in argnames.split(",") if x.strip() - ] - if argname in argnames: - break - else: - metafunc.parametrize( - argname, - fixturedef.params, - indirect=True, - scope=fixturedef.scope, - ids=fixturedef.ids, - ) - else: - continue # Will raise FixtureLookupError at setup time. + metafunc.parametrize( + argname, + fixturedef.params, + indirect=True, + scope=fixturedef.scope, + ids=fixturedef.ids, + ) + break + + # Not requesting the overridden super fixture, stop. + if argname not in fixturedef.argnames: + break + + # Try next super fixture, if any. def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None: # Separate parametrized setups. diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index d54583858ca..9ae5a91db43 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -396,6 +396,132 @@ def test_spam(spam): result = testdir.runpytest(testfile) result.stdout.fnmatch_lines(["*3 passed*"]) + def test_override_fixture_reusing_super_fixture_parametrization(self, testdir): + """Override a fixture at a lower level, reusing the higher-level fixture that + is parametrized (#1953). + """ + testdir.makeconftest( + """ + import pytest + + @pytest.fixture(params=[1, 2]) + def foo(request): + return request.param + """ + ) + testdir.makepyfile( + """ + import pytest + + @pytest.fixture + def foo(foo): + return foo * 2 + + def test_spam(foo): + assert foo in (2, 4) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*2 passed*"]) + + def test_override_parametrize_fixture_and_indirect(self, testdir): + """Override a fixture at a lower level, reusing the higher-level fixture that + is parametrized, while also using indirect parametrization. + """ + testdir.makeconftest( + """ + import pytest + + @pytest.fixture(params=[1, 2]) + def foo(request): + return request.param + """ + ) + testdir.makepyfile( + """ + import pytest + + @pytest.fixture + def foo(foo): + return foo * 2 + + @pytest.fixture + def bar(request): + return request.param * 100 + + @pytest.mark.parametrize("bar", [42], indirect=True) + def test_spam(bar, foo): + assert bar == 4200 + assert foo in (2, 4) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*2 passed*"]) + + def test_override_top_level_fixture_reusing_super_fixture_parametrization( + self, testdir + ): + """Same as the above test, but with another level of overwriting.""" + testdir.makeconftest( + """ + import pytest + + @pytest.fixture(params=['unused', 'unused']) + def foo(request): + return request.param + """ + ) + testdir.makepyfile( + """ + import pytest + + @pytest.fixture(params=[1, 2]) + def foo(request): + return request.param + + class Test: + + @pytest.fixture + def foo(self, foo): + return foo * 2 + + def test_spam(self, foo): + assert foo in (2, 4) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*2 passed*"]) + + def test_override_parametrized_fixture_with_new_parametrized_fixture(self, testdir): + """Overriding a parametrized fixture, while also parametrizing the new fixture and + simultaneously requesting the overwritten fixture as parameter, yields the same value + as ``request.param``. + """ + testdir.makeconftest( + """ + import pytest + + @pytest.fixture(params=['ignored', 'ignored']) + def foo(request): + return request.param + """ + ) + testdir.makepyfile( + """ + import pytest + + @pytest.fixture(params=[10, 20]) + def foo(foo, request): + assert request.param == foo + return foo * 2 + + def test_spam(foo): + assert foo in (20, 40) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*2 passed*"]) + def test_autouse_fixture_plugin(self, testdir): # A fixture from a plugin has no baseid set, which screwed up # the autouse fixture handling. From e36adbaadc4f299efe195b16e5be686ab80cc2a4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 11 Sep 2020 14:37:20 -0300 Subject: [PATCH 620/823] Use ParameterSet to extract argnames from parametrize mark --- src/_pytest/fixtures.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f0e02b8b9d4..44f05d28fc8 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1533,15 +1533,8 @@ def pytest_generate_tests(self, metafunc: "Metafunc") -> None: """Generate new tests based on parametrized fixtures used by the given metafunc""" def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]: - if "argnames" in mark.kwargs: - argnames = mark.kwargs[ - "argnames" - ] # type: Union[str, Tuple[str, ...], List[str]] - else: - argnames = mark.args[0] - if not isinstance(argnames, (tuple, list)): - argnames = [x.strip() for x in argnames.split(",") if x.strip()] - return argnames + args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs) + return args for argname in metafunc.fixturenames: # Get the FixtureDefs for the argname. From e0dd2111a00ea0230f25a17b886581746371817f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 12 Sep 2020 02:03:10 +0300 Subject: [PATCH 621/823] Merge pull request #7720 from pytest-dev/release-6.0.2 Prepare release 6.0.2 (cherry picked from commit fe69bd5baf6b8d1713e16caffc7a5e8dab63bc0f) --- changelog/7148.bugfix.rst | 1 - changelog/7672.bugfix.rst | 1 - changelog/7686.bugfix.rst | 2 -- changelog/7707.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-6.0.2.rst | 19 +++++++++++++++++++ doc/en/changelog.rst | 19 +++++++++++++++++++ doc/en/getting-started.rst | 2 +- 8 files changed, 40 insertions(+), 6 deletions(-) delete mode 100644 changelog/7148.bugfix.rst delete mode 100644 changelog/7672.bugfix.rst delete mode 100644 changelog/7686.bugfix.rst delete mode 100644 changelog/7707.bugfix.rst create mode 100644 doc/en/announce/release-6.0.2.rst diff --git a/changelog/7148.bugfix.rst b/changelog/7148.bugfix.rst deleted file mode 100644 index 71753334c51..00000000000 --- a/changelog/7148.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed ``--log-cli`` potentially causing unrelated ``print`` output to be swallowed. diff --git a/changelog/7672.bugfix.rst b/changelog/7672.bugfix.rst deleted file mode 100644 index 88608e1618d..00000000000 --- a/changelog/7672.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed log-capturing level restored incorrectly if ``caplog.set_level`` is called more than once. diff --git a/changelog/7686.bugfix.rst b/changelog/7686.bugfix.rst deleted file mode 100644 index 8549fae8eb1..00000000000 --- a/changelog/7686.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed `NotSetType.token` being used as the parameter ID when the parametrization list is empty. -Regressed in pytest 6.0.0. diff --git a/changelog/7707.bugfix.rst b/changelog/7707.bugfix.rst deleted file mode 100644 index fbe979d9d6a..00000000000 --- a/changelog/7707.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix internal error when handling some exceptions that contain multiple lines or the style uses multiple lines (``--tb=line`` for example). diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 49d7a0465c8..e011aaa8160 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.0.2 release-6.0.1 release-6.0.0 release-6.0.0rc1 diff --git a/doc/en/announce/release-6.0.2.rst b/doc/en/announce/release-6.0.2.rst new file mode 100644 index 00000000000..16eabc5863d --- /dev/null +++ b/doc/en/announce/release-6.0.2.rst @@ -0,0 +1,19 @@ +pytest-6.0.2 +======================================= + +pytest 6.0.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index ff7fb0c563d..e044af4be0c 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,25 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 6.0.2 (2020-09-04) +========================= + +Bug Fixes +--------- + +- `#7148 `_: Fixed ``--log-cli`` potentially causing unrelated ``print`` output to be swallowed. + + +- `#7672 `_: Fixed log-capturing level restored incorrectly if ``caplog.set_level`` is called more than once. + + +- `#7686 `_: Fixed `NotSetType.token` being used as the parameter ID when the parametrization list is empty. + Regressed in pytest 6.0.0. + + +- `#7707 `_: Fix internal error when handling some exceptions that contain multiple lines or the style uses multiple lines (``--tb=line`` for example). + + pytest 6.0.1 (2020-07-30) ========================= diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index c160191b654..caca6f511de 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 6.0.1 + pytest 6.0.2 .. _`simpletest`: From 96a17b1683fb1bd5b49abff2e4e661084268e672 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 11 Sep 2020 16:44:24 -0700 Subject: [PATCH 622/823] Fix INTERNALERROR when accessing locals / globals with faulty `exec` --- changelog/7742.bugfix.rst | 1 + src/_pytest/_code/code.py | 16 +++++++++++++--- testing/code/test_excinfo.py | 13 +++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 changelog/7742.bugfix.rst diff --git a/changelog/7742.bugfix.rst b/changelog/7742.bugfix.rst new file mode 100644 index 00000000000..95277ee8908 --- /dev/null +++ b/changelog/7742.bugfix.rst @@ -0,0 +1 @@ +Fix INTERNALERROR when accessing locals / globals with faulty ``exec``. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 12d39306a69..98aea8c11ec 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -246,10 +246,20 @@ def ishidden(self) -> bool: Mostly for internal use. """ - f = self.frame - tbh = f.f_locals.get( - "__tracebackhide__", f.f_globals.get("__tracebackhide__", False) + tbh = ( + False ) # type: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] + for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals): + # in normal cases, f_locals and f_globals are dictionaries + # however via `exec(...)` / `eval(...)` they can be other types + # (even incorrect types!). + # as such, we suppress all exceptions while accessing __tracebackhide__ + try: + tbh = maybe_ns_dct["__tracebackhide__"] + except Exception: + pass + else: + break if tbh and callable(tbh): return tbh(None if self._excinfo is None else self._excinfo()) return tbh diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 4dfd6f5cc95..5754977ddc7 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1352,6 +1352,19 @@ def unreraise(): ) assert out == expected_out + def test_exec_type_error_filter(self, importasmod): + """See #7742""" + mod = importasmod( + """\ + def f(): + exec("a = 1", {}, []) + """ + ) + with pytest.raises(TypeError) as excinfo: + mod.f() + # previously crashed with `AttributeError: list has no attribute get` + excinfo.traceback.filter() + @pytest.mark.parametrize("style", ["short", "long"]) @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"]) From 541b30a044fb709e6ab7c7b202237b0d75c7a34e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 12 Sep 2020 14:11:43 -0300 Subject: [PATCH 623/823] Improve docs about plugin discovery/loading at startup Fix #7691 --- doc/en/writing_plugins.rst | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index cf4dbf99fea..ee500253d8a 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -33,26 +33,34 @@ Plugin discovery order at tool startup ``pytest`` loads plugin modules at tool startup in the following way: -* by loading all builtin plugins +1. by scanning the command line for the ``-p no:name`` option + and *blocking* that plugin from being loaded (even builtin plugins can + be blocked this way). This happens before normal command-line parsing. -* by loading all plugins registered through `setuptools entry points`_. +2. by loading all builtin plugins. -* by pre-scanning the command line for the ``-p name`` option - and loading the specified plugin before actual command line parsing. +3. by scanning the command line for the ``-p name`` option + and loading the specified plugin. This happens before normal command-line parsing. -* by loading all :file:`conftest.py` files as inferred by the command line - invocation: +4. by loading all plugins registered through `setuptools entry points`_. - - if no test paths are specified use current dir as a test path - - if exists, load ``conftest.py`` and ``test*/conftest.py`` relative - to the directory part of the first test path. +5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable. - Note that pytest does not find ``conftest.py`` files in deeper nested - sub directories at tool startup. It is usually a good idea to keep - your ``conftest.py`` file in the top level test or project root directory. +6. by loading all :file:`conftest.py` files as inferred by the command line + invocation: -* by recursively loading all plugins specified by the - :globalvar:`pytest_plugins` variable in ``conftest.py`` files + - if no test paths are specified use current dir as a test path + - if exists, load ``conftest.py`` and ``test*/conftest.py`` relative + to the directory part of the first test path. After the ``conftest.py`` + file is loaded, load all plugins specified in its + :globalvar:`pytest_plugins` variable if present. + + Note that pytest does not find ``conftest.py`` files in deeper nested + sub directories at tool startup. It is usually a good idea to keep + your ``conftest.py`` file in the top level test or project root directory. + +7. by recursively loading all plugins specified by the + :globalvar:`pytest_plugins` variable in ``conftest.py`` files. .. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/ From 35350e11cd227ff74fdfdcc856dad10b74fda80b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 12 Sep 2020 22:22:29 +0300 Subject: [PATCH 624/823] assertion/rewrite: rewrite condition to be easier to follow --- src/_pytest/assertion/rewrite.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 48587b17e89..3a7c5f6f26e 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -687,15 +687,15 @@ def run(self, mod: ast.Module) -> None: return expect_docstring = False elif ( - not isinstance(item, ast.ImportFrom) - or item.level > 0 - or item.module != "__future__" + isinstance(item, ast.ImportFrom) + and item.level == 0 + and item.module == "__future__" ): - lineno = item.lineno + pass + else: break pos += 1 - else: - lineno = item.lineno + lineno = item.lineno imports = [ ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases ] From d18cb961cfc57b9b433e67ece546c22f468a07fa Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 12 Sep 2020 22:28:19 +0300 Subject: [PATCH 625/823] assertion/rewrite: fix internal error on collection error due to decorated function For decorated functions, the lineno of the FunctionDef AST node points to the `def` line, not to the first decorator line. On the other hand, in code objects, the `co_firstlineno` points to the first decorator line. Assertion rewriting inserts some imports to code it rewrites. The imports are inserted at the lineno of the first statement in the AST. In turn, the code object compiled from the rewritten AST uses the lineno of the first statement (which is the first inserted import). This means that given a module like this, ```py @foo @bar def baz(): pass ``` the lineno of the code object without assertion rewriting (`--assertion=plain`) is 1, but with assertion rewriting it is 3. And *this* causes some issues for the exception repr when e.g. the decorator line is invalid and raises during collection. The code becomes confused and crashes with INTERNALERROR> File "_pytest/_code/code.py", line 638, in get_source INTERNALERROR> lines.append(space_prefix + source.lines[line_index].strip()) INTERNALERROR> IndexError: list index out of range Fix it by special casing decorators. Maybe there are other cases like this but off hand I can't think of another Python construct where the lineno of the item would be after its first line, and this is the only such issue we have had reported. --- changelog/4984.bugfix.rst | 3 +++ src/_pytest/assertion/rewrite.py | 7 ++++++- testing/test_collection.py | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 changelog/4984.bugfix.rst diff --git a/changelog/4984.bugfix.rst b/changelog/4984.bugfix.rst new file mode 100644 index 00000000000..869a9f244d2 --- /dev/null +++ b/changelog/4984.bugfix.rst @@ -0,0 +1,3 @@ +Fixed an internal error crash with ``IndexError: list index out of range`` when +collecting a module which starts with a decorated function, the decorator +raises, and assertion rewriting is enabled. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 3a7c5f6f26e..5ff57824579 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -695,7 +695,12 @@ def run(self, mod: ast.Module) -> None: else: break pos += 1 - lineno = item.lineno + # Special case: for a decorated function, set the lineno to that of the + # first decorator, not the `def`. Issue #4984. + if isinstance(item, ast.FunctionDef) and item.decorator_list: + lineno = item.decorator_list[0].lineno + else: + lineno = item.lineno imports = [ ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases ] diff --git a/testing/test_collection.py b/testing/test_collection.py index 12030e56e49..3e1b816b79e 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1393,3 +1393,17 @@ def test_modules_not_importable_as_side_effect(self, testdir): "* 1 failed in *", ] ) + + +def test_does_not_crash_on_error_from_decorated_function(testdir: Testdir) -> None: + """Regression test for an issue around bad exception formatting due to + assertion rewriting mangling lineno's (#4984).""" + testdir.makepyfile( + """ + @pytest.fixture + def a(): return 4 + """ + ) + result = testdir.runpytest() + # Not INTERNAL_ERROR + assert result.ret == ExitCode.INTERRUPTED From 24c26a046e541ff758b630c32e411d54e1190062 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 13 Sep 2020 00:41:00 +0100 Subject: [PATCH 626/823] permit tox config in non-tox.ini files for pytest-dev member projects --- CONTRIBUTING.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 50bd0e5e71f..1ad7666ec66 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -151,7 +151,8 @@ the following: - PyPI presence with packaging metadata that contains a ``pytest-`` prefixed name, version number, authors, short and long description. -- a ``tox.ini`` for running tests using `tox `_. +- a `tox configuration `_ + for running tests using `tox `_. - a ``README`` describing how to use the plugin and on which platforms it runs. From 51752108b8d62add4b08a12eba46ca02f833868c Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 13 Sep 2020 03:02:10 +0100 Subject: [PATCH 627/823] remove unused bitbucket link --- CONTRIBUTING.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 50bd0e5e71f..b0b52d81f8c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -131,8 +131,6 @@ in repositories living under the ``pytest-dev`` organisations: - `pytest-dev on GitHub `_ -- `pytest-dev on Bitbucket `_ - All pytest-dev Contributors team members have write access to all contained repositories. Pytest core and plugins are generally developed using `pull requests`_ to respective repositories. From a5fd2895b61d28db15260c63f99fde45f6fe9292 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 13 Sep 2020 12:55:56 -0700 Subject: [PATCH 628/823] Update to deadsnakes/action@v2.0.0 should be a noop (despite the major version bump) -- I rewrote the action to be more stable and need fewer security updates --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 056c8d3dbd9..29d7cd1fece 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -131,7 +131,7 @@ jobs: with: python-version: ${{ matrix.python }} - name: Set up Python ${{ matrix.python }} (deadsnakes) - uses: deadsnakes/action@v1.0.0 + uses: deadsnakes/action@v2.0.0 if: matrix.python == '3.9-dev' with: python-version: ${{ matrix.python }} From 4b46db8ae5bb73d5464d1b6056deca82c73cf173 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 12 Sep 2020 11:30:14 -0300 Subject: [PATCH 629/823] Add full command-line flags to the reference docs Fix #7728 --- doc/en/reference.rst | 293 +++++++++++++++++++++++++++++++++++++++++++ doc/en/usage.rst | 2 + 2 files changed, 295 insertions(+) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index bda5fe6dc22..d1540a8ff2a 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1652,3 +1652,296 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] xfail_strict = True + + +.. _`command-line-flags`: + +Command-line Flags +------------------ + +All the command-line flags can be obtained by running ``pytest --help``:: + + $ pytest --help + usage: pytest [options] [file_or_dir] [file_or_dir] [...] + + positional arguments: + file_or_dir + + general: + -k EXPRESSION only run tests which match the given substring + expression. An expression is a python evaluatable + expression where all names are substring-matched + against test names and their parent classes. + Example: -k 'test_method or test_other' matches all + test functions and classes whose name contains + 'test_method' or 'test_other', while -k 'not + test_method' matches those that don't contain + 'test_method' in their names. -k 'not test_method + and not test_other' will eliminate the matches. + Additionally keywords are matched to classes and + functions containing extra names in their + 'extra_keyword_matches' set, as well as functions + which have names assigned directly to them. The + matching is case-insensitive. + -m MARKEXPR only run tests matching given mark expression. + For example: -m 'mark1 and not mark2'. + --markers show markers (builtin, plugin and per-project ones). + -x, --exitfirst exit instantly on first error or failed test. + --fixtures, --funcargs + show available fixtures, sorted by plugin appearance + (fixtures with leading '_' are only shown with '-v') + --fixtures-per-test show fixtures per test + --pdb start the interactive Python debugger on errors or + KeyboardInterrupt. + --pdbcls=modulename:classname + start a custom interactive Python debugger on + errors. For example: + --pdbcls=IPython.terminal.debugger:TerminalPdb + --trace Immediately break when running each test. + --capture=method per-test capturing method: one of fd|sys|no|tee-sys. + -s shortcut for --capture=no. + --runxfail report the results of xfail tests as if they were + not marked + --lf, --last-failed rerun only the tests that failed at the last run (or + all if none failed) + --ff, --failed-first run all tests, but run the last failures first. + This may re-order tests and thus lead to repeated + fixture setup/teardown. + --nf, --new-first run tests from new files first, then the rest of the + tests sorted by file mtime + --cache-show=[CACHESHOW] + show cache contents, don't perform collection or + tests. Optional argument: glob (default: '*'). + --cache-clear remove all cache contents at start of test run. + --lfnf={all,none}, --last-failed-no-failures={all,none} + which tests to run with no previously (known) + failures. + --sw, --stepwise exit on test failure and continue from last failing + test next time + --stepwise-skip ignore the first failing test but stop on the next + failing test + + reporting: + --durations=N show N slowest setup/test durations (N=0 for all). + --durations-min=N Minimal duration in seconds for inclusion in slowest + list. Default 0.005 + -v, --verbose increase verbosity. + --no-header disable header + --no-summary disable summary + -q, --quiet decrease verbosity. + --verbosity=VERBOSE set verbosity. Default is 0. + -r chars show extra test summary info as specified by chars: + (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, + (p)assed, (P)assed with output, (a)ll except passed + (p/P), or (A)ll. (w)arnings are enabled by default + (see --disable-warnings), 'N' can be used to reset + the list. (default: 'fE'). + --disable-warnings, --disable-pytest-warnings + disable warnings summary + -l, --showlocals show locals in tracebacks (disabled by default). + --tb=style traceback print mode + (auto/long/short/line/native/no). + --show-capture={no,stdout,stderr,log,all} + Controls how captured stdout/stderr/log is shown on + failed tests. Default is 'all'. + --full-trace don't cut any tracebacks (default is to cut). + --color=color color terminal output (yes/no/auto). + --code-highlight={yes,no} + Whether code should be highlighted (only if --color + is also enabled) + --pastebin=mode send failed|all info to bpaste.net pastebin service. + --junit-xml=path create junit-xml style report file at given path. + --junit-prefix=str prepend prefix to classnames in junit-xml output + + pytest-warnings: + -W PYTHONWARNINGS, --pythonwarnings=PYTHONWARNINGS + set which warnings to report, see -W option of + python itself. + --maxfail=num exit after first num failures or errors. + --strict-config any warnings encountered while parsing the `pytest` + section of the configuration file raise errors. + --strict-markers, --strict + markers not registered in the `markers` section of + the configuration file raise errors. + -c file load configuration from `file` instead of trying to + locate one of the implicit configuration files. + --continue-on-collection-errors + Force test execution even if collection errors + occur. + --rootdir=ROOTDIR Define root directory for tests. Can be relative + path: 'root_dir', './root_dir', + 'root_dir/another_dir/'; absolute path: + '/home/user/root_dir'; path with variables: + '$HOME/root_dir'. + + collection: + --collect-only, --co only collect tests, don't execute them. + --pyargs try to interpret all arguments as python packages. + --ignore=path ignore path during collection (multi-allowed). + --ignore-glob=path ignore path pattern during collection (multi- + allowed). + --deselect=nodeid_prefix + deselect item (via node id prefix) during collection + (multi-allowed). + --confcutdir=dir only load conftest.py's relative to specified dir. + --noconftest Don't load any conftest.py files. + --keep-duplicates Keep duplicate tests. + --collect-in-virtualenv + Don't ignore tests in a local virtualenv directory + --import-mode={prepend,append,importlib} + prepend/append to sys.path when importing test + modules and conftest files, default is to prepend. + --doctest-modules run doctests in all .py modules + --doctest-report={none,cdiff,ndiff,udiff,only_first_failure} + choose another output format for diffs on doctest + failure + --doctest-glob=pat doctests file matching pattern, default: test*.txt + --doctest-ignore-import-errors + ignore doctest ImportErrors + --doctest-continue-on-failure + for a given doctest, continue to run after the first + failure + + test session debugging and configuration: + --basetemp=dir base temporary directory for this test run.(warning: + this directory is removed if it exists) + -V, --version display pytest version and information about + plugins.When given twice, also display information + about plugins. + -h, --help show help message and configuration info + -p name early-load given plugin module name or entry point + (multi-allowed). + To avoid loading of plugins, use the `no:` prefix, + e.g. `no:doctest`. + --trace-config trace considerations of conftest.py files. + --debug store internal tracing debug information in + 'pytestdebug.log'. + -o OVERRIDE_INI, --override-ini=OVERRIDE_INI + override ini option with "option=value" style, e.g. + `-o xfail_strict=True -o cache_dir=cache`. + --assert=MODE Control assertion debugging tools. + 'plain' performs no assertion debugging. + 'rewrite' (the default) rewrites assert statements + in test modules on import to provide assert + expression information. + --setup-only only setup fixtures, do not execute tests. + --setup-show show setup of fixtures while executing tests. + --setup-plan show what fixtures and tests would be executed but + don't execute anything. + + logging: + --log-level=LEVEL level of messages to catch/display. + Not set by default, so it depends on the root/parent + log handler's effective level, where it is "WARNING" + by default. + --log-format=LOG_FORMAT + log format as used by the logging module. + --log-date-format=LOG_DATE_FORMAT + log date format as used by the logging module. + --log-cli-level=LOG_CLI_LEVEL + cli logging level. + --log-cli-format=LOG_CLI_FORMAT + log format as used by the logging module. + --log-cli-date-format=LOG_CLI_DATE_FORMAT + log date format as used by the logging module. + --log-file=LOG_FILE path to a file when logging will be written to. + --log-file-level=LOG_FILE_LEVEL + log file logging level. + --log-file-format=LOG_FILE_FORMAT + log format as used by the logging module. + --log-file-date-format=LOG_FILE_DATE_FORMAT + log date format as used by the logging module. + --log-auto-indent=LOG_AUTO_INDENT + Auto-indent multiline messages passed to the logging + module. Accepts true|on, false|off or an integer. + + [pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found: + + markers (linelist): markers for test functions + empty_parameter_set_mark (string): + default marker for empty parametersets + norecursedirs (args): directory patterns to avoid for recursion + testpaths (args): directories to search for tests when no files or + directories are given in the command line. + filterwarnings (linelist): + Each line specifies a pattern for + warnings.filterwarnings. Processed after + -W/--pythonwarnings. + usefixtures (args): list of default fixtures to be used with this + project + python_files (args): glob-style file patterns for Python test module + discovery + python_classes (args): + prefixes or glob names for Python test class + discovery + python_functions (args): + prefixes or glob names for Python test function and + method discovery + disable_test_id_escaping_and_forfeit_all_rights_to_community_support (bool): + disable string escape non-ascii characters, might + cause unwanted side effects(use at your own risk) + console_output_style (string): + console output: "classic", or with additional + progress information ("progress" (percentage) | + "count"). + xfail_strict (bool): default for the strict parameter of xfail markers + when not given explicitly (default: False) + enable_assertion_pass_hook (bool): + Enables the pytest_assertion_pass hook.Make sure to + delete any previously generated pyc cache files. + junit_suite_name (string): + Test suite name for JUnit report + junit_logging (string): + Write captured log messages to JUnit report: one of + no|log|system-out|system-err|out-err|all + junit_log_passing_tests (bool): + Capture log information for passing tests to JUnit + report: + junit_duration_report (string): + Duration time to report: one of total|call + junit_family (string): + Emit XML for schema: one of legacy|xunit1|xunit2 + doctest_optionflags (args): + option flags for doctests + doctest_encoding (string): + encoding used for doctest files + cache_dir (string): cache directory path. + log_level (string): default value for --log-level + log_format (string): default value for --log-format + log_date_format (string): + default value for --log-date-format + log_cli (bool): enable log display during test run (also known as + "live logging"). + log_cli_level (string): + default value for --log-cli-level + log_cli_format (string): + default value for --log-cli-format + log_cli_date_format (string): + default value for --log-cli-date-format + log_file (string): default value for --log-file + log_file_level (string): + default value for --log-file-level + log_file_format (string): + default value for --log-file-format + log_file_date_format (string): + default value for --log-file-date-format + log_auto_indent (string): + default value for --log-auto-indent + faulthandler_timeout (string): + Dump the traceback of all threads if a test takes + more than TIMEOUT seconds to finish. + addopts (args): extra command line options + minversion (string): minimally required pytest version + required_plugins (args): + plugins that must be present for pytest to run + + environment variables: + PYTEST_ADDOPTS extra command line options + PYTEST_PLUGINS comma-separated plugins to load during startup + PYTEST_DISABLE_PLUGIN_AUTOLOAD set to disable plugin auto-loading + PYTEST_DEBUG set to enable debug tracing of pytest's internals + + + to see available markers type: pytest --markers + to see available fixtures type: pytest --fixtures + (shown according to specified file_or_dir or current dir if not specified; fixtures with leading '_' are only shown with the '-v' option diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 1c001060e89..3c03db4540f 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -57,6 +57,8 @@ Getting help on version, option names, environment variables pytest -h | --help # show help on command line and config file options +The full command-line flags can be found in the :ref:`reference `. + .. _maxfail: Stopping after the first (or N) failures From cdf20240703efaab8c14f62fc445e7b239c4d2b8 Mon Sep 17 00:00:00 2001 From: Faris A Chugthai <20028782+farisachugthai@users.noreply.github.com> Date: Tue, 15 Sep 2020 14:41:27 +0000 Subject: [PATCH 630/823] Update writing_plugins.rst (#7757) Capitalize the first word in a sentence and add a period at the end. --- doc/en/writing_plugins.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index cf4dbf99fea..b9b46cb70b2 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -404,7 +404,7 @@ return a result object, with which we can assert the tests' outcomes. result.assert_outcomes(passed=4) -additionally it is possible to copy examples for an example folder before running pytest on it +Additionally it is possible to copy examples for an example folder before running pytest on it. .. code-block:: ini From 7470270f204b489d2cbaf72d477df171b9e43c33 Mon Sep 17 00:00:00 2001 From: Vipul Kumar Date: Wed, 16 Sep 2020 00:37:23 +0000 Subject: [PATCH 631/823] [Docs] remove semi-colon punctuation mark Usually, we use semi-colon punctuation mark to connect closely related ideas. Sentences which are after semicolon, begins with small letter, and last sentence always ends with a period mark, see "Garner's Modern American Usage" book for more information about usage of punctuation mark. So removing punctuation mark altogether is a good idea, as @gnikonorov suggested [1]. [1]: https://github.com/pytest-dev/pytest/pull/7760#pullrequestreview-489232607 --- README.rst | 12 ++++++------ doc/en/index.rst | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 042c87b2fec..057278a926b 100644 --- a/README.rst +++ b/README.rst @@ -77,21 +77,21 @@ Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` stat Features -------- -- Detailed info on failing `assert statements `_ (no need to remember ``self.assert*`` names); +- Detailed info on failing `assert statements `_ (no need to remember ``self.assert*`` names) - `Auto-discovery `_ - of test modules and functions; + of test modules and functions - `Modular fixtures `_ for - managing small or parametrized long-lived test resources; + managing small or parametrized long-lived test resources - Can run `unittest `_ (or trial), - `nose `_ test suites out of the box; + `nose `_ test suites out of the box -- Python 3.5+ and PyPy3; +- Python 3.5+ and PyPy3 -- Rich plugin architecture, with over 850+ `external plugins `_ and thriving community; +- Rich plugin architecture, with over 850+ `external plugins `_ and thriving community Documentation diff --git a/doc/en/index.rst b/doc/en/index.rst index ecfeb753611..a57e9bbacee 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -61,17 +61,17 @@ See :ref:`Getting Started ` for more examples. Features -------- -- Detailed info on failing :ref:`assert statements ` (no need to remember ``self.assert*`` names); +- Detailed info on failing :ref:`assert statements ` (no need to remember ``self.assert*`` names) -- :ref:`Auto-discovery ` of test modules and functions; +- :ref:`Auto-discovery ` of test modules and functions -- :ref:`Modular fixtures ` for managing small or parametrized long-lived test resources; +- :ref:`Modular fixtures ` for managing small or parametrized long-lived test resources -- Can run :ref:`unittest ` (including trial) and :ref:`nose ` test suites out of the box; +- Can run :ref:`unittest ` (including trial) and :ref:`nose ` test suites out of the box -- Python 3.5+ and PyPy 3; +- Python 3.5+ and PyPy 3 -- Rich plugin architecture, with over 315+ `external plugins `_ and thriving community; +- Rich plugin architecture, with over 315+ `external plugins `_ and thriving community Documentation From da21fc5883c041244f32bf5df908a82a295df1ba Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Wed, 16 Sep 2020 11:13:17 +0100 Subject: [PATCH 632/823] Improve output for missing required plugins/unknown config keys (#7723) Co-authored-by: Florian Bruhin --- changelog/7572.improvement.rst | 1 + src/_pytest/config/__init__.py | 3 +-- testing/test_config.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 changelog/7572.improvement.rst diff --git a/changelog/7572.improvement.rst b/changelog/7572.improvement.rst new file mode 100644 index 00000000000..b42da81c1bf --- /dev/null +++ b/changelog/7572.improvement.rst @@ -0,0 +1 @@ +When a plugin listed in ``required_plugins`` is missing, a simple error message is now shown instead of a stacktrace. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4b49a8467b9..2a88d4637ed 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1246,9 +1246,8 @@ def _validate_plugins(self) -> None: missing_plugins.append(required_plugin) if missing_plugins: - fail( + raise UsageError( "Missing required plugins: {}".format(", ".join(missing_plugins)), - pytrace=False, ) def _warn_or_fail_if_strict(self, message: str) -> None: diff --git a/testing/test_config.py b/testing/test_config.py index 64186ffcbef..c1c660ab0f3 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -407,7 +407,7 @@ def my_dists(): testdir.makeini(ini_file_text) if exception_text: - with pytest.raises(pytest.fail.Exception, match=exception_text): + with pytest.raises(pytest.UsageError, match=exception_text): testdir.parseconfig() else: testdir.parseconfig() From 9ceb4e6efc7d98b22ea9356c386862d45b373e8b Mon Sep 17 00:00:00 2001 From: Faris A Chugthai <20028782+farisachugthai@users.noreply.github.com> Date: Wed, 16 Sep 2020 10:18:03 +0000 Subject: [PATCH 633/823] Mistyped was mistyped (#7762) --- doc/en/mark.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/mark.rst b/doc/en/mark.rst index 1cdd1b8e6c4..7370342a965 100644 --- a/doc/en/mark.rst +++ b/doc/en/mark.rst @@ -76,7 +76,7 @@ Raising errors on unknown marks Unregistered marks applied with the ``@pytest.mark.name_of_the_mark`` decorator will always emit a warning in order to avoid silently doing something -surprising due to mis-typed names. As described in the previous section, you can disable +surprising due to mistyped names. As described in the previous section, you can disable the warning for custom marks by registering them in your ``pytest.ini`` file or using a custom ``pytest_configure`` hook. From 895a8cf2969fa66c5167eaa68453771768c472b0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 17 Sep 2020 10:06:04 -0300 Subject: [PATCH 634/823] Add guidelines section about backporting From https://github.com/pytest-dev/pytest/pull/7723#issuecomment-693310439 --- CONTRIBUTING.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index e4a3e7c14df..48ba147b7db 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -406,6 +406,27 @@ actual latest release). The procedure for this is: * Delete the PR body, it usually contains a duplicate commit message. +Who does the backporting +~~~~~~~~~~~~~~~~~~~~~~~~ + +As mentioned above, bugs should first be fixed on ``master`` (except in rare occasions +that a bug only happens in a previous release). So who should do the backport procedure described +above? + +1. If the bug was fixed by a core developer, it is the main responsibility of that core developer + to do the backport. +2. However, often the merge is done by another maintainer, in which case it is nice of them to + do the backport procedure if they have the time. +3. For bugs submitted by non-maintainers, it is expected that a core developer will to do + the backport, normally the one that merged the PR on ``master``. +4. If a non-maintainers notices a bug which is fixed on ``master`` but has not been backported + (due to maintainers forgetting to apply the *needs backport* label, or just plain missing it), + they are also welcome to open a PR with the backport. The procedure is simple and really + helps with the maintenance of the project. + +All the above are not rules, but merely some guidelines/suggestions on what we should expect +about backports. + Handling stale issues/PRs ------------------------- From c4ce5f2c980d079e5d28d542b6c0f3adb5318a46 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 19 Sep 2020 12:36:08 -0300 Subject: [PATCH 635/823] Warning about record_testsuite_property not working with xdist Related to #7767 --- src/_pytest/junitxml.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 0acfb49bc93..877b9be78bc 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -355,6 +355,12 @@ def test_foo(record_testsuite_property): record_testsuite_property("STORAGE_TYPE", "CEPH") ``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped. + + .. warning:: + + Currently this fixture **does not work** with the + `pytest-xdist `__ plugin. See issue + `#7767 `__ for details. """ __tracebackhide__ = True From 89305e7b091a78f6288e0a4bba32f68d2f49bd07 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 19 Sep 2020 17:57:29 +0200 Subject: [PATCH 636/823] Improve output for missing config keys (#7572) Co-authored-by: Bruno Oliveira --- changelog/7572.improvement.rst | 2 +- src/_pytest/config/__init__.py | 2 +- testing/test_config.py | 66 +++++++++++++++++++--------------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/changelog/7572.improvement.rst b/changelog/7572.improvement.rst index b42da81c1bf..e75d6834145 100644 --- a/changelog/7572.improvement.rst +++ b/changelog/7572.improvement.rst @@ -1 +1 @@ -When a plugin listed in ``required_plugins`` is missing, a simple error message is now shown instead of a stacktrace. +When a plugin listed in ``required_plugins`` is missing or an unknown config key is used with ``--strict-config``, a simple error message is now shown instead of a stacktrace. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 2a88d4637ed..0f25b76a68b 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1252,7 +1252,7 @@ def _validate_plugins(self) -> None: def _warn_or_fail_if_strict(self, message: str) -> None: if self.known_args_namespace.strict_config: - fail(message, pytrace=False) + raise UsageError(message) self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3) diff --git a/testing/test_config.py b/testing/test_config.py index c1c660ab0f3..aa84a3cf5d4 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -181,12 +181,12 @@ def test_confcutdir(self, testdir): @pytest.mark.parametrize( "ini_file_text, invalid_keys, warning_output, exception_text", [ - ( + pytest.param( """ - [pytest] - unknown_ini = value1 - another_unknown_ini = value2 - """, + [pytest] + unknown_ini = value1 + another_unknown_ini = value2 + """, ["unknown_ini", "another_unknown_ini"], [ "=*= warnings summary =*=", @@ -194,48 +194,53 @@ def test_confcutdir(self, testdir): "*PytestConfigWarning:*Unknown config option: unknown_ini", ], "Unknown config option: another_unknown_ini", + id="2-unknowns", ), - ( + pytest.param( """ - [pytest] - unknown_ini = value1 - minversion = 5.0.0 - """, + [pytest] + unknown_ini = value1 + minversion = 5.0.0 + """, ["unknown_ini"], [ "=*= warnings summary =*=", "*PytestConfigWarning:*Unknown config option: unknown_ini", ], "Unknown config option: unknown_ini", + id="1-unknown", ), - ( + pytest.param( """ - [some_other_header] - unknown_ini = value1 - [pytest] - minversion = 5.0.0 - """, + [some_other_header] + unknown_ini = value1 + [pytest] + minversion = 5.0.0 + """, [], [], "", + id="unknown-in-other-header", ), - ( + pytest.param( """ - [pytest] - minversion = 5.0.0 - """, + [pytest] + minversion = 5.0.0 + """, [], [], "", + id="no-unknowns", ), - ( + pytest.param( """ - [pytest] - conftest_ini_key = 1 - """, + [pytest] + conftest_ini_key = 1 + """, [], [], "", + id="1-known", ), ], ) @@ -247,9 +252,10 @@ def test_invalid_config_options( """ def pytest_addoption(parser): parser.addini("conftest_ini_key", "") - """ + """ ) - testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) + testdir.makepyfile("def test(): pass") + testdir.makeini(ini_file_text) config = testdir.parseconfig() assert sorted(config._get_unknown_ini_keys()) == sorted(invalid_keys) @@ -257,9 +263,13 @@ def pytest_addoption(parser): result = testdir.runpytest() result.stdout.fnmatch_lines(warning_output) + result = testdir.runpytest("--strict-config") if exception_text: - result = testdir.runpytest("--strict-config") - result.stdout.fnmatch_lines("INTERNALERROR>*" + exception_text) + result.stderr.fnmatch_lines("ERROR: " + exception_text) + assert result.ret == pytest.ExitCode.USAGE_ERROR + else: + result.stderr.no_fnmatch_line(exception_text) + assert result.ret == pytest.ExitCode.OK @pytest.mark.filterwarnings("default") def test_silence_unknown_key_warning(self, testdir: Testdir) -> None: From 027415502a100bb0988131cb45e4d4023c310a3f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 19 Sep 2020 21:35:17 +0300 Subject: [PATCH 637/823] RELEASING: clarify where to push the tag --- RELEASING.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/RELEASING.rst b/RELEASING.rst index 984368618b1..9ff95be92ed 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -26,7 +26,8 @@ A bug-fix release is always done from a maintenance branch, so for example to re Where ``5.1.x`` is the maintenance branch for the ``5.1`` series. -The automated workflow will publish a PR and notify it as a comment in the issue. +The automated workflow will publish a PR for a branch ``release-5.1.2`` +and notify it as a comment in the issue. Minor releases ^^^^^^^^^^^^^^ @@ -41,7 +42,8 @@ Minor releases @pytestbot please prepare release from 5.2.x -The automated workflow will publish a PR and notify it as a comment in the issue. +The automated workflow will publish a PR for a branch ``release-5.2.0`` and +notify it as a comment in the issue. Major and release candidates ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -60,7 +62,8 @@ Major and release candidates @pytestbot please prepare release candidate from 6.0.x -The automated workflow will publish a PR and notify it as a comment in the issue. +The automated workflow will publish a PR for a branch ``release-6.0.0`` and +notify it as a comment in the issue. At this point on, this follows the same workflow as other maintenance branches: bug-fixes are merged into ``master`` and ported back to the maintenance branch, even for release candidates. @@ -101,9 +104,10 @@ Releasing Both automatic and manual processes described above follow the same steps from this point onward. #. After all tests pass and the PR has been approved, tag the release commit - in the ``MAJOR.MINOR.x`` branch and push it. This will publish to PyPI:: + in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI:: - git tag MAJOR.MINOR.PATCH + git fetch --all + git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH git push git@github.com:pytest-dev/pytest.git MAJOR.MINOR.PATCH Wait for the deploy to complete, then make sure it is `available on PyPI `_. From b031a7cecfe66367b0767300f454669b6234508f Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Sat, 19 Sep 2020 19:56:52 +0100 Subject: [PATCH 638/823] Smoke tests for assorted plugins (#7721) Co-authored-by: Bruno Oliveira Co-authored-by: Thomas Grainger Co-authored-by: Kyle Altendorf --- .github/workflows/main.yml | 6 +++ pyproject.toml | 2 +- src/_pytest/config/__init__.py | 4 ++ src/_pytest/python.py | 3 +- testing/plugins_integration/.gitignore | 2 + testing/plugins_integration/README.rst | 13 +++++++ .../plugins_integration/bdd_wallet.feature | 9 +++++ testing/plugins_integration/bdd_wallet.py | 39 +++++++++++++++++++ .../plugins_integration/django_settings.py | 1 + testing/plugins_integration/pytest.ini | 4 ++ .../pytest_anyio_integration.py | 8 ++++ .../pytest_asyncio_integration.py | 8 ++++ .../pytest_mock_integration.py | 2 + .../pytest_trio_integration.py | 8 ++++ .../pytest_twisted_integration.py | 18 +++++++++ .../plugins_integration/simple_integration.py | 10 +++++ testing/test_assertrewrite.py | 4 +- testing/test_config.py | 19 +++++++++ tox.ini | 34 ++++++++++++++++ 19 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 testing/plugins_integration/.gitignore create mode 100644 testing/plugins_integration/README.rst create mode 100644 testing/plugins_integration/bdd_wallet.feature create mode 100644 testing/plugins_integration/bdd_wallet.py create mode 100644 testing/plugins_integration/django_settings.py create mode 100644 testing/plugins_integration/pytest.ini create mode 100644 testing/plugins_integration/pytest_anyio_integration.py create mode 100644 testing/plugins_integration/pytest_asyncio_integration.py create mode 100644 testing/plugins_integration/pytest_mock_integration.py create mode 100644 testing/plugins_integration/pytest_trio_integration.py create mode 100644 testing/plugins_integration/pytest_twisted_integration.py create mode 100644 testing/plugins_integration/simple_integration.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 29d7cd1fece..9ef1a08b993 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,6 +41,7 @@ jobs: "docs", "doctesting", + "plugins", ] include: @@ -111,6 +112,11 @@ jobs: tox_env: "py38-xdist" use_coverage: true + - name: "plugins" + python: "3.7" + os: ubuntu-latest + tox_env: "plugins" + - name: "docs" python: "3.7" os: ubuntu-latest diff --git a/pyproject.toml b/pyproject.toml index 3d3d232f4b8..aee467fcf12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ write_to = "src/_pytest/_version.py" [tool.pytest.ini_options] minversion = "2.0" addopts = "-rfEX -p pytester --strict-markers" -python_files = ["test_*.py", "*_test.py", "testing/*/*.py"] +python_files = ["test_*.py", "*_test.py", "testing/python/*.py"] python_classes = ["Test", "Acceptance"] python_functions = ["test"] # NOTE: "doc" is not included here, but gets tested explicitly via "doctesting". diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 0f25b76a68b..088ec765e67 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1167,6 +1167,10 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: self.pluginmanager.load_setuptools_entrypoints("pytest11") self.pluginmanager.consider_env() + self.known_args_namespace = self._parser.parse_known_args( + args, namespace=copy.copy(self.known_args_namespace) + ) + self._validate_plugins() self._warn_about_skipped_plugins() diff --git a/src/_pytest/python.py b/src/_pytest/python.py index eeccb475555..ea584f3644a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -164,9 +164,10 @@ def async_warn_and_skip(nodeid: str) -> None: msg += ( "You need to install a suitable plugin for your async framework, for example:\n" ) + msg += " - anyio\n" msg += " - pytest-asyncio\n" - msg += " - pytest-trio\n" msg += " - pytest-tornasync\n" + msg += " - pytest-trio\n" msg += " - pytest-twisted" warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid))) skip(msg="async def function and no async plugin installed (see warnings)") diff --git a/testing/plugins_integration/.gitignore b/testing/plugins_integration/.gitignore new file mode 100644 index 00000000000..d934447a03b --- /dev/null +++ b/testing/plugins_integration/.gitignore @@ -0,0 +1,2 @@ +*.html +assets/ diff --git a/testing/plugins_integration/README.rst b/testing/plugins_integration/README.rst new file mode 100644 index 00000000000..8f027c3bd35 --- /dev/null +++ b/testing/plugins_integration/README.rst @@ -0,0 +1,13 @@ +This folder contains tests and support files for smoke testing popular plugins against the current pytest version. + +The objective is to gauge if any intentional or unintentional changes in pytest break plugins. + +As a rule of thumb, we should add plugins here: + +1. That are used at large. This might be subjective in some cases, but if answer is yes to + the question: *if a new release of pytest causes pytest-X to break, will this break a ton of test suites out there?*. +2. That don't have large external dependencies: such as external services. + +Besides adding the plugin as dependency, we should also add a quick test which uses some +minimal part of the plugin, a smoke test. Also consider reusing one of the existing tests if that's +possible. diff --git a/testing/plugins_integration/bdd_wallet.feature b/testing/plugins_integration/bdd_wallet.feature new file mode 100644 index 00000000000..e404c4948e9 --- /dev/null +++ b/testing/plugins_integration/bdd_wallet.feature @@ -0,0 +1,9 @@ +Feature: Buy things with apple + + Scenario: Buy fruits + Given A wallet with 50 + + When I buy some apples for 1 + And I buy some bananas for 2 + + Then I have 47 left diff --git a/testing/plugins_integration/bdd_wallet.py b/testing/plugins_integration/bdd_wallet.py new file mode 100644 index 00000000000..35927ea5875 --- /dev/null +++ b/testing/plugins_integration/bdd_wallet.py @@ -0,0 +1,39 @@ +from pytest_bdd import given +from pytest_bdd import scenario +from pytest_bdd import then +from pytest_bdd import when + +import pytest + + +@scenario("bdd_wallet.feature", "Buy fruits") +def test_publish(): + pass + + +@pytest.fixture +def wallet(): + class Wallet: + amount = 0 + + return Wallet() + + +@given("A wallet with 50") +def fill_wallet(wallet): + wallet.amount = 50 + + +@when("I buy some apples for 1") +def buy_apples(wallet): + wallet.amount -= 1 + + +@when("I buy some bananas for 2") +def buy_bananas(wallet): + wallet.amount -= 2 + + +@then("I have 47 left") +def check(wallet): + assert wallet.amount == 47 diff --git a/testing/plugins_integration/django_settings.py b/testing/plugins_integration/django_settings.py new file mode 100644 index 00000000000..0715f476531 --- /dev/null +++ b/testing/plugins_integration/django_settings.py @@ -0,0 +1 @@ +SECRET_KEY = "mysecret" diff --git a/testing/plugins_integration/pytest.ini b/testing/plugins_integration/pytest.ini new file mode 100644 index 00000000000..f6c77b0dee5 --- /dev/null +++ b/testing/plugins_integration/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = --strict-markers +filterwarnings = + error::pytest.PytestWarning diff --git a/testing/plugins_integration/pytest_anyio_integration.py b/testing/plugins_integration/pytest_anyio_integration.py new file mode 100644 index 00000000000..65c2f593663 --- /dev/null +++ b/testing/plugins_integration/pytest_anyio_integration.py @@ -0,0 +1,8 @@ +import anyio + +import pytest + + +@pytest.mark.anyio +async def test_sleep(): + await anyio.sleep(0) diff --git a/testing/plugins_integration/pytest_asyncio_integration.py b/testing/plugins_integration/pytest_asyncio_integration.py new file mode 100644 index 00000000000..5d2a3faccfc --- /dev/null +++ b/testing/plugins_integration/pytest_asyncio_integration.py @@ -0,0 +1,8 @@ +import asyncio + +import pytest + + +@pytest.mark.asyncio +async def test_sleep(): + await asyncio.sleep(0) diff --git a/testing/plugins_integration/pytest_mock_integration.py b/testing/plugins_integration/pytest_mock_integration.py new file mode 100644 index 00000000000..740469d00fb --- /dev/null +++ b/testing/plugins_integration/pytest_mock_integration.py @@ -0,0 +1,2 @@ +def test_mocker(mocker): + mocker.MagicMock() diff --git a/testing/plugins_integration/pytest_trio_integration.py b/testing/plugins_integration/pytest_trio_integration.py new file mode 100644 index 00000000000..199f7850bc4 --- /dev/null +++ b/testing/plugins_integration/pytest_trio_integration.py @@ -0,0 +1,8 @@ +import trio + +import pytest + + +@pytest.mark.trio +async def test_sleep(): + await trio.sleep(0) diff --git a/testing/plugins_integration/pytest_twisted_integration.py b/testing/plugins_integration/pytest_twisted_integration.py new file mode 100644 index 00000000000..94748d036e5 --- /dev/null +++ b/testing/plugins_integration/pytest_twisted_integration.py @@ -0,0 +1,18 @@ +import pytest_twisted +from twisted.internet.task import deferLater + + +def sleep(): + import twisted.internet.reactor + + return deferLater(clock=twisted.internet.reactor, delay=0) + + +@pytest_twisted.inlineCallbacks +def test_inlineCallbacks(): + yield sleep() + + +@pytest_twisted.ensureDeferred +async def test_inlineCallbacks_async(): + await sleep() diff --git a/testing/plugins_integration/simple_integration.py b/testing/plugins_integration/simple_integration.py new file mode 100644 index 00000000000..20b2fc4b5bb --- /dev/null +++ b/testing/plugins_integration/simple_integration.py @@ -0,0 +1,10 @@ +import pytest + + +def test_foo(): + assert True + + +@pytest.mark.parametrize("i", range(3)) +def test_bar(i): + assert True diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 63a6fdd1285..ad3089a23c0 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -990,7 +990,7 @@ def atomic_write_failed(fn, mode="r", overwrite=False): e = OSError() e.errno = 10 raise e - yield + yield # type:ignore[unreachable] monkeypatch.setattr( _pytest.assertion.rewrite, "atomic_write", atomic_write_failed @@ -1597,7 +1597,7 @@ def test_get_cache_dir(self, monkeypatch, prefix, source, expected): if prefix: if sys.version_info < (3, 8): pytest.skip("pycache_prefix not available in py<38") - monkeypatch.setattr(sys, "pycache_prefix", prefix) + monkeypatch.setattr(sys, "pycache_prefix", prefix) # type:ignore assert get_cache_dir(Path(source)) == Path(expected) diff --git a/testing/test_config.py b/testing/test_config.py index aa84a3cf5d4..0cfd11fd525 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -422,6 +422,25 @@ def my_dists(): else: testdir.parseconfig() + def test_early_config_cmdline(self, testdir, monkeypatch): + """early_config contains options registered by third-party plugins. + + This is a regression involving pytest-cov (and possibly others) introduced in #7700. + """ + testdir.makepyfile( + myplugin=""" + def pytest_addoption(parser): + parser.addoption('--foo', default=None, dest='foo') + + def pytest_load_initial_conftests(early_config, parser, args): + assert early_config.known_args_namespace.foo == "1" + """ + ) + monkeypatch.setenv("PYTEST_PLUGINS", "myplugin") + testdir.syspathinsert() + result = testdir.runpytest("--foo=1") + result.stdout.fnmatch_lines("* no tests ran in *") + class TestConfigCmdlineParsing: def test_parsing_again_fails(self, testdir): diff --git a/tox.ini b/tox.ini index 30aeb27be33..642076dd476 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ envlist = pypy3 py37-{pexpect,xdist,unittestextras,numpy,pluggymaster} doctesting + plugins py37-freeze docs docs-checklinks @@ -114,6 +115,39 @@ commands = rm -rf {envdir}/.pytest_cache make regen +[testenv:plugins] +pip_pre=true +changedir = testing/plugins_integration +deps = + anyio[curio,trio] + django + pytest-asyncio + pytest-bdd + pytest-cov + pytest-django + pytest-flakes + pytest-html + pytest-mock + pytest-sugar + pytest-trio + pytest-twisted + twisted + pytest-xvfb +setenv = + PYTHONPATH=. +commands = + pip check + pytest bdd_wallet.py + pytest --cov=. simple_integration.py + pytest --ds=django_settings simple_integration.py + pytest --html=simple.html simple_integration.py + pytest pytest_anyio_integration.py + pytest pytest_asyncio_integration.py + pytest pytest_mock_integration.py + pytest pytest_trio_integration.py + pytest pytest_twisted_integration.py + pytest simple_integration.py --force-sugar --flakes + [testenv:py37-freeze] changedir = testing/freeze deps = From cdfdb3a25d1033f732072d9a4d2fb700feb73a09 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 19 Sep 2020 16:10:22 -0300 Subject: [PATCH 639/823] Add docs about reusing fixtures from other projects (#7772) Co-authored-by: Ran Benita --- doc/en/fixture.rst | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index b05f60876a2..90e88d87620 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -1136,8 +1136,8 @@ and teared down after every test that used it. .. _`usefixtures`: -Using fixtures from classes, modules or projects ----------------------------------------------------------------------- +Use fixtures in classes and modules with ``usefixtures`` +-------------------------------------------------------- .. regendoc:wipe @@ -1531,3 +1531,37 @@ Given the tests file structure is: In the example above, a parametrized fixture is overridden with a non-parametrized version, and a non-parametrized fixture is overridden with a parametrized version for certain test module. The same applies for the test folder level obviously. + + +Using fixtures from other projects +---------------------------------- + +Usually projects that provide pytest support will use :ref:`entry points `, +so just installing those projects into an environment will make those fixtures available for use. + +In case you want to use fixtures from a project that does not use entry points, you can +define :globalvar:`pytest_plugins` in your top ``conftest.py`` file to register that module +as a plugin. + +Suppose you have some fixtures in ``mylibrary.fixtures`` and you want to reuse them into your +``app/tests`` directory. + +All you need to do is to define :globalvar:`pytest_plugins` in ``app/tests/conftest.py`` +pointing to that module. + +.. code-block:: python + + pytest_plugins = "mylibrary.fixtures" + +This effectively registers ``mylibrary.fixtures`` as a plugin, making all its fixtures and +hooks available to tests in ``app/tests``. + +.. note:: + + Sometimes users will *import* fixtures from other projects for use, however this is not + recommended: importing fixtures into a module will register them in pytest + as *defined* in that module. + + This has minor consequences, such as appearing multiple times in ``pytest --help``, + but it is not **recommended** because this behavior might change/stop working + in future versions. From 7324e901995fd6eca609ac95badb03c9811f6acb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 19 Sep 2020 16:14:28 -0300 Subject: [PATCH 640/823] Update doc/en/writing_plugins.rst Co-authored-by: Ran Benita --- doc/en/writing_plugins.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index ee500253d8a..474f355f651 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -49,7 +49,7 @@ Plugin discovery order at tool startup 6. by loading all :file:`conftest.py` files as inferred by the command line invocation: - - if no test paths are specified use current dir as a test path + - if no test paths are specified, use the current dir as a test path - if exists, load ``conftest.py`` and ``test*/conftest.py`` relative to the directory part of the first test path. After the ``conftest.py`` file is loaded, load all plugins specified in its From 050c2df737f7c6c3b1de7a6f375de107edcb6873 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 21 Sep 2020 15:22:25 -0300 Subject: [PATCH 641/823] Use multiple issue template types and mention Discussions (#7739) --- .../1_bug_report.md} | 10 ++++++++-- .github/ISSUE_TEMPLATE/2_feature_request.md | 5 +++++ .github/ISSUE_TEMPLATE/config.yml | 5 +++++ 3 files changed, 18 insertions(+), 2 deletions(-) rename .github/{ISSUE_TEMPLATE.md => ISSUE_TEMPLATE/1_bug_report.md} (52%) create mode 100644 .github/ISSUE_TEMPLATE/2_feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/1_bug_report.md similarity index 52% rename from .github/ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/1_bug_report.md index fb81416dd5e..0fc3e06cd2c 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE/1_bug_report.md @@ -1,10 +1,16 @@ +--- +name: 🐛 Bug Report +about: Report errors and problems + +--- + -- [ ] a detailed description of the bug or suggestion +- [ ] a detailed description of the bug or problem you are having - [ ] output of `pip list` from the virtual environment you are using - [ ] pytest and operating system versions - [ ] minimal example if possible diff --git a/.github/ISSUE_TEMPLATE/2_feature_request.md b/.github/ISSUE_TEMPLATE/2_feature_request.md new file mode 100644 index 00000000000..54912b05233 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_feature_request.md @@ -0,0 +1,5 @@ +--- +name: 🚀 Feature Request +about: Ideas for new features and improvements + +--- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..742d2e4d668 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: ❓ Support Question + url: https://github.com/pytest-dev/pytest/discussions + about: Use GitHub's new Discussions feature for questions From a99ca879e7c9db0ac91324e701275e9439cf7b73 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 21 Sep 2020 17:45:24 +0300 Subject: [PATCH 642/823] Mark some public and to-be-public classes as `@final` This indicates at least for people using type checkers that these classes are not designed for inheritance and we make no stability guarantees regarding inheritance of them. Currently this doesn't show up in the docs. Sphinx does actually support `@final`, however it only works when imported directly from `typing`, while we import from `_pytest.compat`. In the future there might also be a `@sealed` decorator which would cover some more cases. --- changelog/7780.improvement.rst | 3 +++ src/_pytest/_code/code.py | 2 ++ src/_pytest/_io/terminalwriter.py | 2 ++ src/_pytest/cacheprovider.py | 2 ++ src/_pytest/capture.py | 2 ++ src/_pytest/compat.py | 16 +++++++++++++++- src/_pytest/config/__init__.py | 5 +++++ src/_pytest/config/argparsing.py | 2 ++ src/_pytest/config/exceptions.py | 4 ++++ src/_pytest/fixtures.py | 5 +++++ src/_pytest/logging.py | 2 ++ src/_pytest/main.py | 2 ++ src/_pytest/mark/structures.py | 4 ++++ src/_pytest/monkeypatch.py | 2 ++ src/_pytest/pytester.py | 2 ++ src/_pytest/python.py | 3 +++ src/_pytest/python_api.py | 2 ++ src/_pytest/recwarn.py | 2 ++ src/_pytest/reports.py | 3 +++ src/_pytest/runner.py | 2 ++ src/_pytest/terminal.py | 2 ++ src/_pytest/tmpdir.py | 3 +++ src/_pytest/warning_types.py | 10 ++++++++++ 23 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 changelog/7780.improvement.rst diff --git a/changelog/7780.improvement.rst b/changelog/7780.improvement.rst new file mode 100644 index 00000000000..6651387b14e --- /dev/null +++ b/changelog/7780.improvement.rst @@ -0,0 +1,3 @@ +Public classes which are not designed to be inherited from are now marked `@final `_. +Code which inherits from these classes will trigger a type-checking (e.g. mypy) error, but will still work in runtime. +Currently the ``final`` designation does not appear in the API Reference but hopefully will in the future. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 98aea8c11ec..5063e660477 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -38,6 +38,7 @@ from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr from _pytest.compat import ATTRS_EQ_FIELD +from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING @@ -414,6 +415,7 @@ def recursionindex(self) -> Optional[int]: _E = TypeVar("_E", bound=BaseException, covariant=True) +@final @attr.s(repr=False) class ExceptionInfo(Generic[_E]): """Wraps sys.exc_info() objects and offers help for navigating the traceback.""" diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 0afe4a0eda4..a9404ebcc16 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -7,6 +7,7 @@ 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. @@ -36,6 +37,7 @@ def should_do_markup(file: TextIO) -> bool: ) +@final class TerminalWriter: _esctable = dict( black=30, diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index ba27735d039..b04305ed9d2 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -21,6 +21,7 @@ from .reports import CollectReport from _pytest import nodes from _pytest._io import TerminalWriter +from _pytest.compat import final from _pytest.compat import order_preserving_dict from _pytest.config import Config from _pytest.config import ExitCode @@ -50,6 +51,7 @@ """ +@final @attr.s class Cache: _cachedir = attr.ib(type=Path, repr=False) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 3bf3bc923ef..2d2b392aba8 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -17,6 +17,7 @@ from typing import Union import pytest +from _pytest.compat import final from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config.argparsing import Parser @@ -498,6 +499,7 @@ def writeorg(self, data): # pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can # make it a namedtuple again. # [0]: https://github.com/python/mypy/issues/685 +@final @functools.total_ordering class CaptureResult(Generic[AnyStr]): """The result of :method:`CaptureFixture.readouterr`.""" diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 0c9f47de707..7eab2ea0c85 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -19,7 +19,6 @@ import attr -from _pytest._io.saferepr import saferepr from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -297,6 +296,8 @@ def get_real_func(obj): break obj = new_obj else: + from _pytest._io.saferepr import saferepr + raise ValueError( ("could not find real function of {start}\nstopped at {current}").format( start=saferepr(start_obj), current=saferepr(obj) @@ -357,6 +358,19 @@ def overload(f): # noqa: F811 return f +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): # noqa: F811 + return f + + if getattr(attr, "__version_info__", ()) >= (19, 2): ATTRS_EQ_FIELD = "eq" else: diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 088ec765e67..f89ed37027b 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -43,6 +43,7 @@ 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 from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import fail @@ -76,6 +77,7 @@ hookspec = HookspecMarker("pytest") +@final class ExitCode(enum.IntEnum): """Encodes the valid exit codes by pytest. @@ -322,6 +324,7 @@ def _prepareconfig( raise +@final class PytestPluginManager(PluginManager): """A :py:class:`pluggy.PluginManager ` with additional pytest-specific functionality: @@ -815,6 +818,7 @@ def _args_converter(args: Iterable[str]) -> Tuple[str, ...]: return tuple(args) +@final class Config: """Access to configuration values, pluginmanager and plugin hooks. @@ -825,6 +829,7 @@ class Config: invocation. """ + @final @attr.s(frozen=True) class InvocationParams: """Holds parameters passed during :func:`pytest.main`. diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 6c6feff4206..636021df455 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -16,6 +16,7 @@ import py import _pytest._io +from _pytest.compat import final from _pytest.compat import TYPE_CHECKING from _pytest.config.exceptions import UsageError @@ -26,6 +27,7 @@ FILE_OR_DIR = "file_or_dir" +@final class Parser: """Parser for command line arguments and ini-file values. diff --git a/src/_pytest/config/exceptions.py b/src/_pytest/config/exceptions.py index 95c412734be..4f1320e758d 100644 --- a/src/_pytest/config/exceptions.py +++ b/src/_pytest/config/exceptions.py @@ -1,3 +1,7 @@ +from _pytest.compat import final + + +@final class UsageError(Exception): """Error in pytest usage or invocation.""" diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 44f05d28fc8..f526f484b29 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -32,6 +32,7 @@ from _pytest._io import TerminalWriter from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper +from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.compat import get_real_method from _pytest.compat import getfuncargnames @@ -730,6 +731,7 @@ def __repr__(self) -> str: return "" % (self.node) +@final class SubRequest(FixtureRequest): """A sub request for handling getting a fixture from a test function/fixture.""" @@ -796,6 +798,7 @@ def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int: ) +@final class FixtureLookupError(LookupError): """Could not return a requested fixture (missing or invalid).""" @@ -952,6 +955,7 @@ def _eval_scope_callable( return result +@final class FixtureDef(Generic[_FixtureValue]): """A container for a factory definition.""" @@ -1161,6 +1165,7 @@ def result(*args, **kwargs): return result +@final @attr.s(frozen=True) class FixtureFunctionMarker: scope = attr.ib(type="Union[_Scope, Callable[[str, Config], _Scope]]") diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 98386bacda0..c277ba5320c 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -19,6 +19,7 @@ from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.capture import CaptureManager +from _pytest.compat import final from _pytest.compat import nullcontext from _pytest.config import _strtobool from _pytest.config import Config @@ -339,6 +340,7 @@ def handleError(self, record: logging.LogRecord) -> None: raise +@final class LogCaptureFixture: """Provides access and control of log capturing.""" diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 4e35990adb3..ef106c46a43 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -21,6 +21,7 @@ import _pytest._code from _pytest import nodes +from _pytest.compat import final from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING from _pytest.config import Config @@ -435,6 +436,7 @@ def __missing__(self, path: Path) -> str: return r +@final class Session(nodes.FSCollector): Interrupted = Interrupted Failed = Failed diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 73e1f77ce74..39a2321b3ff 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -20,6 +20,7 @@ from .._code import getfslineno from ..compat import ascii_escaped +from ..compat import final from ..compat import NOTSET from ..compat import NotSetType from ..compat import overload @@ -199,6 +200,7 @@ def _for_parametrize( return argnames, parameters +@final @attr.s(frozen=True) class Mark: #: Name of the mark. @@ -452,6 +454,7 @@ def __call__( # type: ignore[override] ... +@final class MarkGenerator: """Factory for :class:`MarkDecorator` objects - exposed as a ``pytest.mark`` singleton instance. @@ -525,6 +528,7 @@ def __getattr__(self, name: str) -> MarkDecorator: # TODO(py36): inherit from typing.MutableMapping[str, Any]. +@final class NodeKeywords(collections.abc.MutableMapping): # type: ignore[type-arg] def __init__(self, node: "Node") -> None: self.node = node diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 1f324986b68..bbd96779da5 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -14,6 +14,7 @@ from typing import Union import pytest +from _pytest.compat import final from _pytest.compat import overload from _pytest.fixtures import fixture from _pytest.pathlib import Path @@ -110,6 +111,7 @@ def __repr__(self) -> str: notset = Notset() +@final class MonkeyPatch: """Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.""" diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 5d8a45ad702..d78062a86ce 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -28,6 +28,7 @@ from _pytest import timing from _pytest._code import Source from _pytest.capture import _get_multicapture +from _pytest.compat import final from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin @@ -597,6 +598,7 @@ def restore(self) -> None: sys.path[:], sys.meta_path[:] = self.__saved +@final class Testdir: """Temporary test directory with tools to test/run pytest itself. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ea584f3644a..7d3e301c076 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -37,6 +37,7 @@ from _pytest._io import TerminalWriter from _pytest._io.saferepr import saferepr from _pytest.compat import ascii_escaped +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 @@ -864,6 +865,7 @@ def hasnew(obj: object) -> bool: return False +@final class CallSpec2: def __init__(self, metafunc: "Metafunc") -> None: self.metafunc = metafunc @@ -924,6 +926,7 @@ def setmulti2( self.marks.extend(normalize_mark_list(marks)) +@final class Metafunc: """Objects passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index a1eb29e1aba..f5ad04a12c9 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -17,6 +17,7 @@ from typing import Union import _pytest._code +from _pytest.compat import final from _pytest.compat import overload from _pytest.compat import STRING_TYPES from _pytest.compat import TYPE_CHECKING @@ -699,6 +700,7 @@ def raises( # noqa: F811 raises.Exception = fail.Exception # type: ignore +@final class RaisesContext(Generic[_E]): def __init__( self, diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 3668de627e6..39d6de91455 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -13,6 +13,7 @@ from typing import TypeVar from typing import Union +from _pytest.compat import final from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING from _pytest.fixtures import fixture @@ -228,6 +229,7 @@ def __exit__( self._entered = False +@final class WarningsChecker(WarningsRecorder): def __init__( self, diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 48caa6ceebe..c42f778ec40 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -26,6 +26,7 @@ from _pytest._code.code import ReprTraceback from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter +from _pytest.compat import final from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.nodes import Collector @@ -225,6 +226,7 @@ def _report_unserialization_failure( raise RuntimeError(stream.getvalue()) +@final class TestReport(BaseReport): """Basic test report object (also used for setup and teardown calls if they fail).""" @@ -333,6 +335,7 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": ) +@final class CollectReport(BaseReport): """Collection report object.""" diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 2dc940b395e..f29d356fe07 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -22,6 +22,7 @@ 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.compat import TYPE_CHECKING from _pytest.config.argparsing import Parser from _pytest.nodes import Collector @@ -259,6 +260,7 @@ def call_runtest_hook( TResult = TypeVar("TResult", covariant=True) +@final @attr.s(repr=False) class CallInfo(Generic[TResult]): """Result/Exception info a function invocation. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 59d6aa97d03..e059612c212 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -32,6 +32,7 @@ from _pytest._code import ExceptionInfo from _pytest._code.code import ExceptionRepr from _pytest._io.wcwidth import wcswidth +from _pytest.compat import final from _pytest.compat import order_preserving_dict from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin @@ -309,6 +310,7 @@ def get_location(self, config: Config) -> Optional[str]: return None +@final class TerminalReporter: def __init__(self, config: Config, file: Optional[TextIO] = None) -> None: import _pytest.config diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 7eb19b59e57..eb8aa9f9104 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -13,11 +13,13 @@ from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup from .pathlib import Path +from _pytest.compat import final from _pytest.config import Config from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch +@final @attr.s class TempPathFactory: """Factory for temporary directories under the common base temp directory. @@ -103,6 +105,7 @@ def getbasetemp(self) -> Path: return t +@final @attr.s class TempdirFactory: """Backward comptibility wrapper that implements :class:``py.path.local`` diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index c93b9604907..52e4d2b14cb 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -4,6 +4,7 @@ import attr +from _pytest.compat import final from _pytest.compat import TYPE_CHECKING if TYPE_CHECKING: @@ -16,36 +17,42 @@ class PytestWarning(UserWarning): __module__ = "pytest" +@final class PytestAssertRewriteWarning(PytestWarning): """Warning emitted by the pytest assert rewrite module.""" __module__ = "pytest" +@final class PytestCacheWarning(PytestWarning): """Warning emitted by the cache plugin in various situations.""" __module__ = "pytest" +@final class PytestConfigWarning(PytestWarning): """Warning emitted for configuration issues.""" __module__ = "pytest" +@final class PytestCollectionWarning(PytestWarning): """Warning emitted when pytest is not able to collect a file or symbol in a module.""" __module__ = "pytest" +@final class PytestDeprecationWarning(PytestWarning, DeprecationWarning): """Warning class for features that will be removed in a future version.""" __module__ = "pytest" +@final class PytestExperimentalApiWarning(PytestWarning, FutureWarning): """Warning category used to denote experiments in pytest. @@ -64,6 +71,7 @@ def simple(cls, apiname: str) -> "PytestExperimentalApiWarning": ) +@final class PytestUnhandledCoroutineWarning(PytestWarning): """Warning emitted for an unhandled coroutine. @@ -75,6 +83,7 @@ class PytestUnhandledCoroutineWarning(PytestWarning): __module__ = "pytest" +@final class PytestUnknownMarkWarning(PytestWarning): """Warning emitted on use of unknown markers. @@ -87,6 +96,7 @@ class PytestUnknownMarkWarning(PytestWarning): _W = TypeVar("_W", bound=PytestWarning) +@final @attr.s class UnformattedWarning(Generic[_W]): """A warning meant to be formatted during runtime. From 821562513526dcd439b253f25677cc2c998d8fef Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 23 Sep 2020 09:14:27 -0300 Subject: [PATCH 643/823] Use new pip resolver in plugins tox env Fix #7783 --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index 642076dd476..b42aecdf85c 100644 --- a/tox.ini +++ b/tox.ini @@ -116,7 +116,11 @@ commands = make regen [testenv:plugins] +# use latest versions of all plugins, including pre-releases pip_pre=true +# use latest pip and new dependency resolver (#7783) +download=true +install_command=python -m pip --use-feature=2020-resolver install {opts} {packages} changedir = testing/plugins_integration deps = anyio[curio,trio] From d3f47bf3468c964d4e41258c55db6577d99c7992 Mon Sep 17 00:00:00 2001 From: Kamran Ahmad <60498143+kamahmad@users.noreply.github.com> Date: Wed, 23 Sep 2020 19:45:55 +0530 Subject: [PATCH 644/823] Improved 'Declaring new hooks' section in docs. (#7782) Co-authored-by: Bruno Oliveira --- AUTHORS | 1 + doc/en/writing_plugins.rst | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/AUTHORS b/AUTHORS index b28e5613389..c8dfec4010a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -151,6 +151,7 @@ Joshua Bronson Jurko Gospodnetić Justyna Janczyszyn Kale Kundert +Kamran Ahmad Karl O. Pinc Katarzyna Jachim Katarzyna Król diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index b9b46cb70b2..625ced7bd2f 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -614,6 +614,11 @@ among each other. Declaring new hooks ------------------------ +.. note:: + + This is a quick overview on how to add new hooks and how they work in general, but a more complete + overview can be found in `the pluggy documentation `__. + .. currentmodule:: _pytest.hookspec Plugins and ``conftest.py`` files may declare new hooks that can then be @@ -627,7 +632,7 @@ Hooks are usually declared as do-nothing functions that contain only documentation describing when the hook will be called and what return values are expected. The names of the functions must start with `pytest_` otherwise pytest won't recognize them. -Here's an example. Let's assume this code is in the ``hooks.py`` module. +Here's an example. Let's assume this code is in the ``sample_hook.py`` module. .. code-block:: python @@ -643,10 +648,10 @@ class or module can then be passed to the ``pluginmanager`` using the ``pytest_a .. code-block:: python def pytest_addhooks(pluginmanager): - """ This example assumes the hooks are grouped in the 'hooks' module. """ - from my_app.tests import hooks + """ This example assumes the hooks are grouped in the 'sample_hook' module. """ + from my_app.tests import sample_hook - pluginmanager.add_hookspecs(hooks) + pluginmanager.add_hookspecs(sample_hook) For a real world example, see `newhooks.py`_ from `xdist `_. From d3c746eb8eb96e68e1c2624a46d99b9e60d455ec Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Sep 2020 20:04:17 +0300 Subject: [PATCH 645/823] changelog: some consistency cleanups --- changelog/1953.bugfix.rst | 2 +- changelog/5585.breaking.rst | 2 +- changelog/6981.deprecation.rst | 2 +- changelog/7097.deprecation.rst | 12 ++++++------ changelog/7210.deprecation.rst | 10 +++++----- changelog/7255.deprecation.rst | 3 ++- changelog/7628.bugfix.rst | 2 +- changelog/7671.trivial.rst | 6 +++--- changelog/7742.bugfix.rst | 2 +- 9 files changed, 21 insertions(+), 20 deletions(-) diff --git a/changelog/1953.bugfix.rst b/changelog/1953.bugfix.rst index 9db33ab10f5..850b8b35475 100644 --- a/changelog/1953.bugfix.rst +++ b/changelog/1953.bugfix.rst @@ -1,4 +1,4 @@ -Fix error when overwriting a parametrized fixture, while also reusing the super fixture value. +Fixed error when overwriting a parametrized fixture, while also reusing the super fixture value. .. code-block:: python diff --git a/changelog/5585.breaking.rst b/changelog/5585.breaking.rst index 0ecba32df81..a0e7cab8c0f 100644 --- a/changelog/5585.breaking.rst +++ b/changelog/5585.breaking.rst @@ -1,4 +1,4 @@ -As per our policy, the following features have been deprecated in the 5.X series and are now +As per our policy, the following features which have been deprecated in the 5.X series are now removed: * The ``funcargnames`` read-only property of ``FixtureRequest``, ``Metafunc``, and ``Function`` classes. Use ``fixturenames`` attribute. diff --git a/changelog/6981.deprecation.rst b/changelog/6981.deprecation.rst index 622dd9500ae..28cb10fe51b 100644 --- a/changelog/6981.deprecation.rst +++ b/changelog/6981.deprecation.rst @@ -1 +1 @@ -Deprecate the ``pytest.collect`` module: all its names can be imported from ``pytest`` directly. +The ``pytest.collect`` module is deprecated: all its names can be imported from ``pytest`` directly. diff --git a/changelog/7097.deprecation.rst b/changelog/7097.deprecation.rst index b2aba597b61..f7f38e3c821 100644 --- a/changelog/7097.deprecation.rst +++ b/changelog/7097.deprecation.rst @@ -1,6 +1,6 @@ -The ``pytest._fillfuncargs`` function is now deprecated. This function was kept -for backward compatibility with an older plugin. - -It's functionality is not meant to be used directly, but if you must replace -it, use `function._request._fillfixtures()` instead, though note this is not -a public API and may break in the future. +The ``pytest._fillfuncargs`` function is deprecated. This function was kept +for backward compatibility with an older plugin. + +It's functionality is not meant to be used directly, but if you must replace +it, use `function._request._fillfixtures()` instead, though note this is not +a public API and may break in the future. diff --git a/changelog/7210.deprecation.rst b/changelog/7210.deprecation.rst index be0ead2214e..3e1350eaa79 100644 --- a/changelog/7210.deprecation.rst +++ b/changelog/7210.deprecation.rst @@ -1,5 +1,5 @@ -The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'`` -instead. - -The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue -if you use this and want a replacement. +The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'`` +instead. + +The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue +if you use this and want a replacement. diff --git a/changelog/7255.deprecation.rst b/changelog/7255.deprecation.rst index c6d56ab5a07..120590b1227 100644 --- a/changelog/7255.deprecation.rst +++ b/changelog/7255.deprecation.rst @@ -1 +1,2 @@ -The :func:`pytest_warning_captured` hook has been deprecated in favor of :func:`pytest_warning_recorded`, and will be removed in a future version. +The :func:`pytest_warning_captured <_pytest.hookspec.pytest_warning_captured>` hook is deprecated in favor +of :func:`pytest_warning_recorded <_pytest.hookspec.pytest_warning_recorded>`, and will be removed in a future version. diff --git a/changelog/7628.bugfix.rst b/changelog/7628.bugfix.rst index 9f3480aaa65..7e020f01209 100644 --- a/changelog/7628.bugfix.rst +++ b/changelog/7628.bugfix.rst @@ -1 +1 @@ -Fix test collection when a full path without a drive letter was passed to pytest on Windows (for example ``\projects\tests\test.py`` instead of ``c:\projects\tests\pytest.py``). +Fixed test collection when a full path without a drive letter was passed to pytest on Windows (for example ``\projects\tests\test.py`` instead of ``c:\projects\tests\pytest.py``). diff --git a/changelog/7671.trivial.rst b/changelog/7671.trivial.rst index 6dddf4cf042..de9b8ef13d2 100644 --- a/changelog/7671.trivial.rst +++ b/changelog/7671.trivial.rst @@ -1,6 +1,6 @@ When collecting tests, pytest finds test classes and functions by examining the attributes of python objects (modules, classes and instances). To speed up this process, pytest now ignores builtin attributes (like ``__class__``, -``__delattr__`` and ``__new__``) without consulting the ``python_classes`` and -``python_functions`` configuration options and without passing them to plugins -using the ``pytest_pycollect_makeitem`` hook. +``__delattr__`` and ``__new__``) without consulting the :confval:`python_classes` and +:confval:`python_functions` configuration options and without passing them to plugins +using the :func:`pytest_pycollect_makeitem <_pytest.hookspec.pytest_pycollect_makeitem>` hook. diff --git a/changelog/7742.bugfix.rst b/changelog/7742.bugfix.rst index 95277ee8908..1f73a443106 100644 --- a/changelog/7742.bugfix.rst +++ b/changelog/7742.bugfix.rst @@ -1 +1 @@ -Fix INTERNALERROR when accessing locals / globals with faulty ``exec``. +Fixed INTERNALERROR when accessing locals / globals with faulty ``exec``. From 19c78ab57440a77b532f3dfa54ea6ed2d1eb0376 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Sep 2020 21:09:34 +0300 Subject: [PATCH 646/823] Merge pull request #7797 from pytest-dev/release-6.1.0 Prepare release 6.1.0 (cherry picked from commit 08a1ab3a8acdfdeffd8f07058b44743df1d90150) --- changelog/1477.doc.rst | 1 - changelog/1953.bugfix.rst | 20 ---- changelog/4984.bugfix.rst | 3 - changelog/5585.breaking.rst | 18 ---- changelog/6681.improvement.rst | 3 - changelog/6981.deprecation.rst | 1 - changelog/7097.deprecation.rst | 6 -- changelog/7210.deprecation.rst | 5 - changelog/7255.deprecation.rst | 2 - changelog/7536.trivial.rst | 3 - changelog/7572.improvement.rst | 1 - changelog/7587.trivial.rst | 1 - changelog/7591.bugfix.rst | 1 - changelog/7628.bugfix.rst | 1 - changelog/7631.trivial.rst | 2 - changelog/7638.bugfix.rst | 1 - changelog/7648.deprecation.rst | 3 - changelog/7667.feature.rst | 1 - changelog/7671.trivial.rst | 6 -- changelog/7685.improvement.rst | 3 - changelog/7742.bugfix.rst | 1 - changelog/7780.improvement.rst | 3 - doc/en/announce/index.rst | 1 + doc/en/announce/release-6.1.0.rst | 44 ++++++++ doc/en/builtin.rst | 54 +++++----- doc/en/changelog.rst | 160 ++++++++++++++++++++++++++++++ doc/en/getting-started.rst | 2 +- 27 files changed, 237 insertions(+), 110 deletions(-) delete mode 100644 changelog/1477.doc.rst delete mode 100644 changelog/1953.bugfix.rst delete mode 100644 changelog/4984.bugfix.rst delete mode 100644 changelog/5585.breaking.rst delete mode 100644 changelog/6681.improvement.rst delete mode 100644 changelog/6981.deprecation.rst delete mode 100644 changelog/7097.deprecation.rst delete mode 100644 changelog/7210.deprecation.rst delete mode 100644 changelog/7255.deprecation.rst delete mode 100644 changelog/7536.trivial.rst delete mode 100644 changelog/7572.improvement.rst delete mode 100644 changelog/7587.trivial.rst delete mode 100644 changelog/7591.bugfix.rst delete mode 100644 changelog/7628.bugfix.rst delete mode 100644 changelog/7631.trivial.rst delete mode 100644 changelog/7638.bugfix.rst delete mode 100644 changelog/7648.deprecation.rst delete mode 100644 changelog/7667.feature.rst delete mode 100644 changelog/7671.trivial.rst delete mode 100644 changelog/7685.improvement.rst delete mode 100644 changelog/7742.bugfix.rst delete mode 100644 changelog/7780.improvement.rst create mode 100644 doc/en/announce/release-6.1.0.rst diff --git a/changelog/1477.doc.rst b/changelog/1477.doc.rst deleted file mode 100644 index fbe12597f07..00000000000 --- a/changelog/1477.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Removed faq.rst and its reference in contents.rst. diff --git a/changelog/1953.bugfix.rst b/changelog/1953.bugfix.rst deleted file mode 100644 index 850b8b35475..00000000000 --- a/changelog/1953.bugfix.rst +++ /dev/null @@ -1,20 +0,0 @@ -Fixed error when overwriting a parametrized fixture, while also reusing the super fixture value. - -.. code-block:: python - - # conftest.py - import pytest - - - @pytest.fixture(params=[1, 2]) - def foo(request): - return request.param - - - # test_foo.py - import pytest - - - @pytest.fixture - def foo(foo): - return foo * 2 diff --git a/changelog/4984.bugfix.rst b/changelog/4984.bugfix.rst deleted file mode 100644 index 869a9f244d2..00000000000 --- a/changelog/4984.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed an internal error crash with ``IndexError: list index out of range`` when -collecting a module which starts with a decorated function, the decorator -raises, and assertion rewriting is enabled. diff --git a/changelog/5585.breaking.rst b/changelog/5585.breaking.rst deleted file mode 100644 index a0e7cab8c0f..00000000000 --- a/changelog/5585.breaking.rst +++ /dev/null @@ -1,18 +0,0 @@ -As per our policy, the following features which have been deprecated in the 5.X series are now -removed: - -* The ``funcargnames`` read-only property of ``FixtureRequest``, ``Metafunc``, and ``Function`` classes. Use ``fixturenames`` attribute. - -* ``@pytest.fixture`` no longer supports positional arguments, pass all arguments by keyword instead. - -* Direct construction of ``Node`` subclasses now raise an error, use ``from_parent`` instead. - -* The default value for ``junit_family`` has changed to ``xunit2``. If you require the old format, add ``junit_family=xunit1`` to your configuration file. - -* The ``TerminalReporter`` no longer has a ``writer`` attribute. Plugin authors may use the public functions of the ``TerminalReporter`` instead of accessing the ``TerminalWriter`` object directly. - -* The ``--result-log`` option has been removed. Users are recommended to use the `pytest-reportlog `__ plugin instead. - - -For more information consult -`Deprecations and Removals `__ in the docs. diff --git a/changelog/6681.improvement.rst b/changelog/6681.improvement.rst deleted file mode 100644 index cc586e6a337..00000000000 --- a/changelog/6681.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -Internal pytest warnings issued during the early stages of initialization are now properly handled and can filtered through :confval:`filterwarnings` or ``--pythonwarnings/-W``. - -This also fixes a number of long standing issues: `#2891 `__, `#7620 `__, `#7426 `__. diff --git a/changelog/6981.deprecation.rst b/changelog/6981.deprecation.rst deleted file mode 100644 index 28cb10fe51b..00000000000 --- a/changelog/6981.deprecation.rst +++ /dev/null @@ -1 +0,0 @@ -The ``pytest.collect`` module is deprecated: all its names can be imported from ``pytest`` directly. diff --git a/changelog/7097.deprecation.rst b/changelog/7097.deprecation.rst deleted file mode 100644 index f7f38e3c821..00000000000 --- a/changelog/7097.deprecation.rst +++ /dev/null @@ -1,6 +0,0 @@ -The ``pytest._fillfuncargs`` function is deprecated. This function was kept -for backward compatibility with an older plugin. - -It's functionality is not meant to be used directly, but if you must replace -it, use `function._request._fillfixtures()` instead, though note this is not -a public API and may break in the future. diff --git a/changelog/7210.deprecation.rst b/changelog/7210.deprecation.rst deleted file mode 100644 index 3e1350eaa79..00000000000 --- a/changelog/7210.deprecation.rst +++ /dev/null @@ -1,5 +0,0 @@ -The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'`` -instead. - -The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue -if you use this and want a replacement. diff --git a/changelog/7255.deprecation.rst b/changelog/7255.deprecation.rst deleted file mode 100644 index 120590b1227..00000000000 --- a/changelog/7255.deprecation.rst +++ /dev/null @@ -1,2 +0,0 @@ -The :func:`pytest_warning_captured <_pytest.hookspec.pytest_warning_captured>` hook is deprecated in favor -of :func:`pytest_warning_recorded <_pytest.hookspec.pytest_warning_recorded>`, and will be removed in a future version. diff --git a/changelog/7536.trivial.rst b/changelog/7536.trivial.rst deleted file mode 100644 index f713da4f1a0..00000000000 --- a/changelog/7536.trivial.rst +++ /dev/null @@ -1,3 +0,0 @@ -The internal ``junitxml`` plugin has rewritten to use ``xml.etree.ElementTree``. -The order of attributes in XML elements might differ. Some unneeded escaping is -no longer performed. diff --git a/changelog/7572.improvement.rst b/changelog/7572.improvement.rst deleted file mode 100644 index e75d6834145..00000000000 --- a/changelog/7572.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -When a plugin listed in ``required_plugins`` is missing or an unknown config key is used with ``--strict-config``, a simple error message is now shown instead of a stacktrace. diff --git a/changelog/7587.trivial.rst b/changelog/7587.trivial.rst deleted file mode 100644 index 1477c97caef..00000000000 --- a/changelog/7587.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -The dependency on the ``more-itertools`` package has been removed. diff --git a/changelog/7591.bugfix.rst b/changelog/7591.bugfix.rst deleted file mode 100644 index 10de43a96a5..00000000000 --- a/changelog/7591.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -pylint shouldn't complain anymore about unimplemented abstract methods when inheriting from :ref:`File `. diff --git a/changelog/7628.bugfix.rst b/changelog/7628.bugfix.rst deleted file mode 100644 index 7e020f01209..00000000000 --- a/changelog/7628.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed test collection when a full path without a drive letter was passed to pytest on Windows (for example ``\projects\tests\test.py`` instead of ``c:\projects\tests\pytest.py``). diff --git a/changelog/7631.trivial.rst b/changelog/7631.trivial.rst deleted file mode 100644 index 81e1d71cc3c..00000000000 --- a/changelog/7631.trivial.rst +++ /dev/null @@ -1,2 +0,0 @@ -The result type of :meth:`capfd.readouterr() <_pytest.capture.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple, -but should behave like one in all respects. This was done for technical reasons. diff --git a/changelog/7638.bugfix.rst b/changelog/7638.bugfix.rst deleted file mode 100644 index ea3257b6771..00000000000 --- a/changelog/7638.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix handling of command-line options that appear as paths but trigger an OS-level syntax error on Windows, such as the options used internally by ``pytest-xdist``. diff --git a/changelog/7648.deprecation.rst b/changelog/7648.deprecation.rst deleted file mode 100644 index 440b1114116..00000000000 --- a/changelog/7648.deprecation.rst +++ /dev/null @@ -1,3 +0,0 @@ -The ``gethookproxy()`` and ``isinitpath()`` methods of ``FSCollector`` and ``Package`` are deprecated; -use ``self.session.gethookproxy()`` and ``self.session.isinitpath()`` instead. -This should work on all pytest versions. diff --git a/changelog/7667.feature.rst b/changelog/7667.feature.rst deleted file mode 100644 index 3928495e51f..00000000000 --- a/changelog/7667.feature.rst +++ /dev/null @@ -1 +0,0 @@ -New ``--durations-min`` command-line flag controls the minimal duration for inclusion in the slowest list of tests shown by ``--durations``. Previously this was hard-coded to ``0.005s``. diff --git a/changelog/7671.trivial.rst b/changelog/7671.trivial.rst deleted file mode 100644 index de9b8ef13d2..00000000000 --- a/changelog/7671.trivial.rst +++ /dev/null @@ -1,6 +0,0 @@ -When collecting tests, pytest finds test classes and functions by examining the -attributes of python objects (modules, classes and instances). To speed up this -process, pytest now ignores builtin attributes (like ``__class__``, -``__delattr__`` and ``__new__``) without consulting the :confval:`python_classes` and -:confval:`python_functions` configuration options and without passing them to plugins -using the :func:`pytest_pycollect_makeitem <_pytest.hookspec.pytest_pycollect_makeitem>` hook. diff --git a/changelog/7685.improvement.rst b/changelog/7685.improvement.rst deleted file mode 100644 index 59772162489..00000000000 --- a/changelog/7685.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -Added two new attributes :attr:`rootpath <_pytest.config.Config.rootpath>` and :attr:`inipath <_pytest.config.Config.inipath>` to :class:`Config <_pytest.config.Config>`. -These attributes are :class:`pathlib.Path` versions of the existing :attr:`rootdir <_pytest.config.Config.rootdir>` and :attr:`inifile <_pytest.config.Config.inifile>` attributes, -and should be preferred over them when possible. diff --git a/changelog/7742.bugfix.rst b/changelog/7742.bugfix.rst deleted file mode 100644 index 1f73a443106..00000000000 --- a/changelog/7742.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed INTERNALERROR when accessing locals / globals with faulty ``exec``. diff --git a/changelog/7780.improvement.rst b/changelog/7780.improvement.rst deleted file mode 100644 index 6651387b14e..00000000000 --- a/changelog/7780.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -Public classes which are not designed to be inherited from are now marked `@final `_. -Code which inherits from these classes will trigger a type-checking (e.g. mypy) error, but will still work in runtime. -Currently the ``final`` designation does not appear in the API Reference but hopefully will in the future. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index e011aaa8160..753e81156ab 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.1.0 release-6.0.2 release-6.0.1 release-6.0.0 diff --git a/doc/en/announce/release-6.1.0.rst b/doc/en/announce/release-6.1.0.rst new file mode 100644 index 00000000000..f4b571ae846 --- /dev/null +++ b/doc/en/announce/release-6.1.0.rst @@ -0,0 +1,44 @@ +pytest-6.1.0 +======================================= + +The pytest team is proud to announce the 6.1.0 release! + +This release contains new features, improvements, bug fixes, and breaking changes, so users +are encouraged to take a look at the CHANGELOG carefully: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Anthony Sottile +* Bruno Oliveira +* C. Titus Brown +* Drew Devereux +* Faris A Chugthai +* Florian Bruhin +* Hugo van Kemenade +* Hynek Schlawack +* Joseph Lucas +* Kamran Ahmad +* Mattreex +* Maximilian Cosmo Sitter +* Ran Benita +* Rüdiger Busche +* Sam Estep +* Sorin Sbarnea +* Thomas Grainger +* Vipul Kumar +* Yutaro Ikeda +* hp310780 + + +Happy testing, +The pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index b33ee041dd6..0fd58164c76 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -23,7 +23,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a cache.get(key, default) cache.set(key, value) - Keys must be a ``/`` separated value, where the first part is usually the + Keys must be ``/`` separated strings, where the first part is usually the name of your plugin or application to avoid clashes with other cache users. Values can be any object handled by the json stdlib module. @@ -57,7 +57,8 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a ``out`` and ``err`` will be ``byte`` objects. doctest_namespace [session scope] - Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. + Fixture that returns a :py:class:`dict` that will be injected into the + namespace of doctests. pytestconfig [session scope] Session-scoped fixture that returns the :class:`_pytest.config.Config` object. @@ -89,8 +90,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a automatically XML-encoded. record_testsuite_property [session scope] - Records a new ```` tag as child of the root ````. This is suitable to - writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family. + Record a new ```` tag as child of the root ````. + + This is suitable to writing global information regarding the entire test + suite, and is compatible with ``xunit2`` JUnit family. This is a ``session``-scoped fixture which is called with ``(name, value)``. Example: @@ -102,6 +105,12 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a ``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped. + .. warning:: + + Currently this fixture **does not work** with the + `pytest-xdist `__ plugin. See issue + `#7767 `__ for details. + caplog Access and control log capturing. @@ -114,8 +123,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a * caplog.clear() -> clear captured records and formatted log output string monkeypatch - The returned ``monkeypatch`` fixture provides these - helper methods to modify objects, dictionaries or os.environ:: + A convenient fixture for monkey-patching. + + The fixture provides these methods to modify objects, dictionaries or + os.environ:: monkeypatch.setattr(obj, name, value, raising=True) monkeypatch.delattr(obj, name, raising=True) @@ -126,10 +137,9 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a monkeypatch.syspath_prepend(path) monkeypatch.chdir(path) - All modifications will be undone after the requesting - test function or fixture has finished. The ``raising`` - parameter determines if a KeyError or AttributeError - will be raised if the set/deletion operation has no target. + All modifications will be undone after the requesting test function or + fixture has finished. The ``raising`` parameter determines if a KeyError + or AttributeError will be raised if the set/deletion operation has no target. recwarn Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. @@ -140,30 +150,28 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a tmpdir_factory [session scope] Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session. - tmp_path_factory [session scope] Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session. - tmpdir - Return a temporary directory path object - which is unique to each test function invocation, - created as a sub directory of the base temporary - directory. The returned object is a `py.path.local`_ - path object. + Return a temporary directory path object which is unique to each test + function invocation, created as a sub directory of the base temporary + directory. + + The returned object is a `py.path.local`_ path object. .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html tmp_path - Return a temporary directory path object - which is unique to each test function invocation, - created as a sub directory of the base temporary - directory. The returned object is a :class:`pathlib.Path` - object. + Return a temporary directory path object which is unique to each test + function invocation, created as a sub directory of the base temporary + directory. + + The returned object is a :class:`pathlib.Path` object. .. note:: - in python < 3.6 this is a pathlib2.Path + In python < 3.6 this is a pathlib2.Path. no tests ran in 0.12s diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index e044af4be0c..c620d271ff9 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,166 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 6.1.0 (2020-09-26) +========================= + +Breaking Changes +---------------- + +- `#5585 `_: As per our policy, the following features which have been deprecated in the 5.X series are now + removed: + + * The ``funcargnames`` read-only property of ``FixtureRequest``, ``Metafunc``, and ``Function`` classes. Use ``fixturenames`` attribute. + + * ``@pytest.fixture`` no longer supports positional arguments, pass all arguments by keyword instead. + + * Direct construction of ``Node`` subclasses now raise an error, use ``from_parent`` instead. + + * The default value for ``junit_family`` has changed to ``xunit2``. If you require the old format, add ``junit_family=xunit1`` to your configuration file. + + * The ``TerminalReporter`` no longer has a ``writer`` attribute. Plugin authors may use the public functions of the ``TerminalReporter`` instead of accessing the ``TerminalWriter`` object directly. + + * The ``--result-log`` option has been removed. Users are recommended to use the `pytest-reportlog `__ plugin instead. + + + For more information consult + `Deprecations and Removals `__ in the docs. + + + +Deprecations +------------ + +- `#6981 `_: The ``pytest.collect`` module is deprecated: all its names can be imported from ``pytest`` directly. + + +- `#7097 `_: The ``pytest._fillfuncargs`` function is deprecated. This function was kept + for backward compatibility with an older plugin. + + It's functionality is not meant to be used directly, but if you must replace + it, use `function._request._fillfixtures()` instead, though note this is not + a public API and may break in the future. + + +- `#7210 `_: The special ``-k '-expr'`` syntax to ``-k`` is deprecated. Use ``-k 'not expr'`` + instead. + + The special ``-k 'expr:'`` syntax to ``-k`` is deprecated. Please open an issue + if you use this and want a replacement. + + +- `#7255 `_: The :func:`pytest_warning_captured <_pytest.hookspec.pytest_warning_captured>` hook is deprecated in favor + of :func:`pytest_warning_recorded <_pytest.hookspec.pytest_warning_recorded>`, and will be removed in a future version. + + +- `#7648 `_: The ``gethookproxy()`` and ``isinitpath()`` methods of ``FSCollector`` and ``Package`` are deprecated; + use ``self.session.gethookproxy()`` and ``self.session.isinitpath()`` instead. + This should work on all pytest versions. + + + +Features +-------- + +- `#7667 `_: New ``--durations-min`` command-line flag controls the minimal duration for inclusion in the slowest list of tests shown by ``--durations``. Previously this was hard-coded to ``0.005s``. + + + +Improvements +------------ + +- `#6681 `_: Internal pytest warnings issued during the early stages of initialization are now properly handled and can filtered through :confval:`filterwarnings` or ``--pythonwarnings/-W``. + + This also fixes a number of long standing issues: `#2891 `__, `#7620 `__, `#7426 `__. + + +- `#7572 `_: When a plugin listed in ``required_plugins`` is missing or an unknown config key is used with ``--strict-config``, a simple error message is now shown instead of a stacktrace. + + +- `#7685 `_: Added two new attributes :attr:`rootpath <_pytest.config.Config.rootpath>` and :attr:`inipath <_pytest.config.Config.inipath>` to :class:`Config <_pytest.config.Config>`. + These attributes are :class:`pathlib.Path` versions of the existing :attr:`rootdir <_pytest.config.Config.rootdir>` and :attr:`inifile <_pytest.config.Config.inifile>` attributes, + and should be preferred over them when possible. + + +- `#7780 `_: Public classes which are not designed to be inherited from are now marked `@final `_. + Code which inherits from these classes will trigger a type-checking (e.g. mypy) error, but will still work in runtime. + Currently the ``final`` designation does not appear in the API Reference but hopefully will in the future. + + + +Bug Fixes +--------- + +- `#1953 `_: Fixed error when overwriting a parametrized fixture, while also reusing the super fixture value. + + .. code-block:: python + + # conftest.py + import pytest + + + @pytest.fixture(params=[1, 2]) + def foo(request): + return request.param + + + # test_foo.py + import pytest + + + @pytest.fixture + def foo(foo): + return foo * 2 + + +- `#4984 `_: Fixed an internal error crash with ``IndexError: list index out of range`` when + collecting a module which starts with a decorated function, the decorator + raises, and assertion rewriting is enabled. + + +- `#7591 `_: pylint shouldn't complain anymore about unimplemented abstract methods when inheriting from :ref:`File `. + + +- `#7628 `_: Fixed test collection when a full path without a drive letter was passed to pytest on Windows (for example ``\projects\tests\test.py`` instead of ``c:\projects\tests\pytest.py``). + + +- `#7638 `_: Fix handling of command-line options that appear as paths but trigger an OS-level syntax error on Windows, such as the options used internally by ``pytest-xdist``. + + +- `#7742 `_: Fixed INTERNALERROR when accessing locals / globals with faulty ``exec``. + + + +Improved Documentation +---------------------- + +- `#1477 `_: Removed faq.rst and its reference in contents.rst. + + + +Trivial/Internal Changes +------------------------ + +- `#7536 `_: The internal ``junitxml`` plugin has rewritten to use ``xml.etree.ElementTree``. + The order of attributes in XML elements might differ. Some unneeded escaping is + no longer performed. + + +- `#7587 `_: The dependency on the ``more-itertools`` package has been removed. + + +- `#7631 `_: The result type of :meth:`capfd.readouterr() <_pytest.capture.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple, + but should behave like one in all respects. This was done for technical reasons. + + +- `#7671 `_: When collecting tests, pytest finds test classes and functions by examining the + attributes of python objects (modules, classes and instances). To speed up this + process, pytest now ignores builtin attributes (like ``__class__``, + ``__delattr__`` and ``__new__``) without consulting the :confval:`python_classes` and + :confval:`python_functions` configuration options and without passing them to plugins + using the :func:`pytest_pycollect_makeitem <_pytest.hookspec.pytest_pycollect_makeitem>` hook. + + pytest 6.0.2 (2020-09-04) ========================= diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index caca6f511de..c8730b9a370 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 6.0.2 + pytest 6.1.0 .. _`simpletest`: From 14de6781d89a5ec17a2f2486aa27b41d16f8bf3d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 26 Sep 2020 23:46:58 +0300 Subject: [PATCH 647/823] Fix typos in pytestbot --- scripts/release-on-comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release-on-comment.py b/scripts/release-on-comment.py index 7c662113e06..e2ddbe1ca04 100644 --- a/scripts/release-on-comment.py +++ b/scripts/release-on-comment.py @@ -227,7 +227,7 @@ def find_next_version(base_branch: str, is_major: bool) -> str: msg = dedent( f""" Found features or breaking changes in `{base_branch}`, and feature releases can only be - created from `master`.": + created from `master`: """ ) msg += "\n".join(f"* `{x.name}`" for x in sorted(features + breaking)) From 32bb8f3a63ceaf19c925fc941d1fb872abd1b36a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 23 Sep 2020 14:28:10 +0300 Subject: [PATCH 648/823] Bump attrs requirement from >=17.4.0 to >=19.2.0 This allows us to remove the `ATTRS_EQ_FIELD` thing which is causing some annoyance. --- .github/workflows/main.yml | 2 +- changelog/7802.trivial.rst | 1 + setup.cfg | 2 +- src/_pytest/_code/code.py | 21 ++++++++++----------- src/_pytest/assertion/util.py | 5 +---- src/_pytest/compat.py | 6 ------ testing/test_assertion.py | 3 +-- testing/test_meta.py | 2 -- testing/test_unittest.py | 6 +----- tox.ini | 2 -- 10 files changed, 16 insertions(+), 34 deletions(-) create mode 100644 changelog/7802.trivial.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9ef1a08b993..84b7de5f8e8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -79,7 +79,7 @@ jobs: - name: "ubuntu-py37" python: "3.7" os: ubuntu-latest - tox_env: "py37-lsof-numpy-oldattrs-pexpect" + tox_env: "py37-lsof-numpy-pexpect" use_coverage: true - name: "ubuntu-py37-pluggy" python: "3.7" diff --git a/changelog/7802.trivial.rst b/changelog/7802.trivial.rst new file mode 100644 index 00000000000..1f8bc2c9dc6 --- /dev/null +++ b/changelog/7802.trivial.rst @@ -0,0 +1 @@ +The ``attrs`` dependency requirement is now >=19.2.0 instead of >=17.4.0. diff --git a/setup.cfg b/setup.cfg index f4170f15ae2..6b54fc37096 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,7 @@ packages = _pytest.mark pytest install_requires = - attrs>=17.4.0 + attrs>=19.2.0 iniconfig packaging pluggy>=0.12,<1.0 diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 5063e660477..6b908bb51e9 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -37,7 +37,6 @@ from _pytest._io import TerminalWriter from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr -from _pytest.compat import ATTRS_EQ_FIELD from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.compat import overload @@ -918,7 +917,7 @@ def repr_excinfo( return ExceptionChainRepr(repr_chain) -@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore +@attr.s(eq=False) class TerminalRepr: def __str__(self) -> str: # FYI this is called from pytest-xdist's serialization of exception @@ -936,7 +935,7 @@ def toterminal(self, tw: TerminalWriter) -> None: # This class is abstract -- only subclasses are instantiated. -@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore +@attr.s(eq=False) class ExceptionRepr(TerminalRepr): # Provided by subclasses. reprcrash = None # type: Optional[ReprFileLocation] @@ -954,7 +953,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line(content) -@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore +@attr.s(eq=False) class ExceptionChainRepr(ExceptionRepr): chain = attr.ib( type=Sequence[ @@ -978,7 +977,7 @@ def toterminal(self, tw: TerminalWriter) -> None: super().toterminal(tw) -@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore +@attr.s(eq=False) class ReprExceptionInfo(ExceptionRepr): reprtraceback = attr.ib(type="ReprTraceback") reprcrash = attr.ib(type="ReprFileLocation") @@ -988,7 +987,7 @@ def toterminal(self, tw: TerminalWriter) -> None: super().toterminal(tw) -@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore +@attr.s(eq=False) class ReprTraceback(TerminalRepr): reprentries = attr.ib(type=Sequence[Union["ReprEntry", "ReprEntryNative"]]) extraline = attr.ib(type=Optional[str]) @@ -1022,7 +1021,7 @@ def __init__(self, tblines: Sequence[str]) -> None: self.extraline = None -@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore +@attr.s(eq=False) class ReprEntryNative(TerminalRepr): lines = attr.ib(type=Sequence[str]) style = "native" # type: _TracebackStyle @@ -1031,7 +1030,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.write("".join(self.lines)) -@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore +@attr.s(eq=False) class ReprEntry(TerminalRepr): lines = attr.ib(type=Sequence[str]) reprfuncargs = attr.ib(type=Optional["ReprFuncArgs"]) @@ -1111,7 +1110,7 @@ def __str__(self) -> str: ) -@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore +@attr.s(eq=False) class ReprFileLocation(TerminalRepr): path = attr.ib(type=str, converter=str) lineno = attr.ib(type=int) @@ -1128,7 +1127,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line(":{}: {}".format(self.lineno, msg)) -@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore +@attr.s(eq=False) class ReprLocals(TerminalRepr): lines = attr.ib(type=Sequence[str]) @@ -1137,7 +1136,7 @@ def toterminal(self, tw: TerminalWriter, indent="") -> None: tw.line(indent + line) -@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore +@attr.s(eq=False) class ReprFuncArgs(TerminalRepr): args = attr.ib(type=Sequence[Tuple[str, object]]) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index e80e476c84a..1d0e903cdf8 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -16,7 +16,6 @@ from _pytest._io.saferepr import _pformat_dispatch from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr -from _pytest.compat import ATTRS_EQ_FIELD # The _reprcompare attribute on the util module is used by the new assertion # interpretation code and assertion rewriter to detect this plugin was @@ -420,9 +419,7 @@ def _compare_eq_cls( fields_to_check = [field for field, info in all_fields.items() if info.compare] elif isattrs(left): all_fields = left.__attrs_attrs__ - fields_to_check = [ - field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD) - ] + fields_to_check = [field.name for field in all_fields if getattr(field, "eq")] indent = " " same = [] diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 7eab2ea0c85..33221aac2d0 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -371,12 +371,6 @@ def final(f): # noqa: F811 return f -if getattr(attr, "__version_info__", ()) >= (19, 2): - ATTRS_EQ_FIELD = "eq" -else: - ATTRS_EQ_FIELD = "cmp" - - if sys.version_info >= (3, 8): from functools import cached_property as cached_property else: diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 1cb63a329f6..258be48b82f 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -12,7 +12,6 @@ from _pytest import outcomes from _pytest.assertion import truncate from _pytest.assertion import util -from _pytest.compat import ATTRS_EQ_FIELD def mock_config(verbose=0): @@ -945,7 +944,7 @@ def test_attrs_with_attribute_comparison_off(self): @attr.s class SimpleDataObject: field_a = attr.ib() - field_b = attr.ib(**{ATTRS_EQ_FIELD: False}) # type: ignore + field_b = attr.ib(eq=False) left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "b") diff --git a/testing/test_meta.py b/testing/test_meta.py index 1acf6d09f59..97b2e1a1a49 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -27,8 +27,6 @@ def test_no_warnings(module: str) -> None: subprocess.check_call(( sys.executable, "-W", "error", - # https://github.com/pytest-dev/pytest/issues/5901 - "-W", "ignore:The usage of `cmp` is deprecated and will be removed on or after 2021-06-01. Please use `eq` and `order` instead.:DeprecationWarning", # noqa: E501 "-c", "__import__({!r})".format(module), )) # fmt: on diff --git a/testing/test_unittest.py b/testing/test_unittest.py index c7b6bfcec92..f3f4d4e06ac 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -532,11 +532,7 @@ def f(_): # will crash both at test time and at teardown """ ) - # Ignore DeprecationWarning (for `cmp`) from attrs through twisted, - # for stable test results. - result = testdir.runpytest( - "-vv", "-oconsole_output_style=classic", "-W", "ignore::DeprecationWarning" - ) + result = testdir.runpytest("-vv", "-oconsole_output_style=classic") result.stdout.fnmatch_lines( [ "test_trial_error.py::TC::test_four FAILED", diff --git a/tox.ini b/tox.ini index b42aecdf85c..3e96ef49b97 100644 --- a/tox.ini +++ b/tox.ini @@ -45,8 +45,6 @@ setenv = extras = testing deps = doctesting: PyYAML - oldattrs: attrs==17.4.0 - oldattrs: hypothesis<=4.38.1 numpy: numpy pexpect: pexpect pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master From 91fa11bed093329edb1006def90d17f545092ab6 Mon Sep 17 00:00:00 2001 From: Jakob van Santen Date: Mon, 28 Sep 2020 17:17:23 +0200 Subject: [PATCH 649/823] python_api: let approx() take nonnumeric values (#7710) Co-authored-by: Bruno Oliveira --- AUTHORS | 1 + changelog/7710.improvement.rst | 3 ++ src/_pytest/python_api.py | 66 +++++++++++++++++++++++++-------- testing/python/approx.py | 68 +++++++++++++++++++++++++++++++--- 4 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 changelog/7710.improvement.rst diff --git a/AUTHORS b/AUTHORS index c8dfec4010a..ab84a3e5289 100644 --- a/AUTHORS +++ b/AUTHORS @@ -129,6 +129,7 @@ Ilya Konstantinov Ionuț Turturică Iwan Briquemont Jaap Broekhuizen +Jakob van Santen Jakub Mitoraj Jan Balster Janne Vanhala diff --git a/changelog/7710.improvement.rst b/changelog/7710.improvement.rst new file mode 100644 index 00000000000..1bbaf7792ab --- /dev/null +++ b/changelog/7710.improvement.rst @@ -0,0 +1,3 @@ +Use strict equality comparison for nonnumeric types in ``approx`` instead of +raising ``TypeError``. +This was the undocumented behavior before 3.7, but is now officially a supported feature. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index f5ad04a12c9..681f83028d8 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -4,7 +4,7 @@ from collections.abc import Mapping from collections.abc import Sized from decimal import Decimal -from numbers import Number +from numbers import Complex from types import TracebackType from typing import Any from typing import Callable @@ -146,7 +146,10 @@ def __repr__(self) -> str: ) def __eq__(self, actual) -> bool: - if set(actual.keys()) != set(self.expected.keys()): + try: + if set(actual.keys()) != set(self.expected.keys()): + return False + except AttributeError: return False return ApproxBase.__eq__(self, actual) @@ -161,8 +164,6 @@ def _check_type(self) -> None: if isinstance(value, type(self.expected)): msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}" raise TypeError(msg.format(key, value, pprint.pformat(self.expected))) - elif not isinstance(value, Number): - raise _non_numeric_type_error(self.expected, at="key={!r}".format(key)) class ApproxSequencelike(ApproxBase): @@ -177,7 +178,10 @@ def __repr__(self) -> str: ) def __eq__(self, actual) -> bool: - if len(actual) != len(self.expected): + try: + if len(actual) != len(self.expected): + return False + except TypeError: return False return ApproxBase.__eq__(self, actual) @@ -190,10 +194,6 @@ def _check_type(self) -> None: if isinstance(x, type(self.expected)): msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}" raise TypeError(msg.format(x, index, pprint.pformat(self.expected))) - elif not isinstance(x, Number): - raise _non_numeric_type_error( - self.expected, at="index {}".format(index) - ) class ApproxScalar(ApproxBase): @@ -211,16 +211,23 @@ def __repr__(self) -> str: For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``. """ - # Infinities aren't compared using tolerances, so don't show a - # tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j). - if math.isinf(abs(self.expected)): + # Don't show a tolerance for values that aren't compared using + # tolerances, i.e. non-numerics and infinities. Need to call abs to + # handle complex numbers, e.g. (inf + 1j). + if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf( + abs(self.expected) + ): return str(self.expected) # If a sensible tolerance can't be calculated, self.tolerance will # raise a ValueError. In this case, display '???'. try: vetted_tolerance = "{:.1e}".format(self.tolerance) - if isinstance(self.expected, complex) and not math.isinf(self.tolerance): + if ( + isinstance(self.expected, Complex) + and self.expected.imag + and not math.isinf(self.tolerance) + ): vetted_tolerance += " ∠ ±180°" except ValueError: vetted_tolerance = "???" @@ -239,6 +246,15 @@ def __eq__(self, actual) -> bool: if actual == self.expected: return True + # If either type is non-numeric, fall back to strict equality. + # NB: we need Complex, rather than just Number, to ensure that __abs__, + # __sub__, and __float__ are defined. + if not ( + isinstance(self.expected, (Complex, Decimal)) + and isinstance(actual, (Complex, Decimal)) + ): + return False + # Allow the user to control whether NaNs are considered equal to each # other or not. The abs() calls are for compatibility with complex # numbers. @@ -409,6 +425,18 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: >>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12) True + You can also use ``approx`` to compare nonnumeric types, or dicts and + sequences containing nonnumeric types, in which case it falls back to + strict equality. This can be useful for comparing dicts and sequences that + can contain optional values:: + + >>> {"required": 1.0000005, "optional": None} == approx({"required": 1, "optional": None}) + True + >>> [None, 1.0000005] == approx([None,1]) + True + >>> ["foo", 1.0000005] == approx([None,1]) + False + If you're thinking about using ``approx``, then you might want to know how it compares to other good ways of comparing floating-point numbers. All of these algorithms are based on relative and absolute tolerances and should @@ -466,6 +494,14 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: follows a fixed behavior. `More information...`__ __ https://docs.python.org/3/reference/datamodel.html#object.__ge__ + + .. versionchanged:: 3.7.1 + ``approx`` raises ``TypeError`` when it encounters a dict value or + sequence element of nonnumeric type. + + .. versionchanged:: 6.1.0 + ``approx`` falls back to strict equality for nonnumeric types instead + of raising ``TypeError``. """ # Delegate the comparison to a class that knows how to deal with the type @@ -487,8 +523,6 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: if isinstance(expected, Decimal): cls = ApproxDecimal # type: Type[ApproxBase] - elif isinstance(expected, Number): - cls = ApproxScalar elif isinstance(expected, Mapping): cls = ApproxMapping elif _is_numpy_array(expected): @@ -501,7 +535,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: ): cls = ApproxSequencelike else: - raise _non_numeric_type_error(expected, at=None) + cls = ApproxScalar return cls(expected, rel, abs, nan_ok) diff --git a/testing/python/approx.py b/testing/python/approx.py index 194423dc3b0..5f12da37654 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -1,4 +1,5 @@ import operator +import sys from decimal import Decimal from fractions import Fraction from operator import eq @@ -329,6 +330,9 @@ def test_tuple_wrong_len(self): assert (1, 2) != approx((1,)) assert (1, 2) != approx((1, 2, 3)) + def test_tuple_vs_other(self): + assert 1 != approx((1,)) + def test_dict(self): actual = {"a": 1 + 1e-7, "b": 2 + 1e-8} # Dictionaries became ordered in python3.6, so switch up the order here @@ -346,6 +350,13 @@ def test_dict_wrong_len(self): assert {"a": 1, "b": 2} != approx({"a": 1, "c": 2}) assert {"a": 1, "b": 2} != approx({"a": 1, "b": 2, "c": 3}) + def test_dict_nonnumeric(self): + assert {"a": 1.0, "b": None} == pytest.approx({"a": 1.0, "b": None}) + assert {"a": 1.0, "b": 1} != pytest.approx({"a": 1.0, "b": None}) + + def test_dict_vs_other(self): + assert 1 != approx({"a": 0}) + def test_numpy_array(self): np = pytest.importorskip("numpy") @@ -463,20 +474,67 @@ def test_foo(): ["*At index 0 diff: 3 != 4 ± {}".format(expected), "=* 1 failed in *="] ) + @pytest.mark.parametrize( + "x, name", + [ + pytest.param([[1]], "data structures", id="nested-list"), + pytest.param({"key": {"key": 1}}, "dictionaries", id="nested-dict"), + ], + ) + def test_expected_value_type_error(self, x, name): + with pytest.raises( + TypeError, + match=r"pytest.approx\(\) does not support nested {}:".format(name), + ): + approx(x) + @pytest.mark.parametrize( "x", [ pytest.param(None), pytest.param("string"), pytest.param(["string"], id="nested-str"), - pytest.param([[1]], id="nested-list"), pytest.param({"key": "string"}, id="dict-with-string"), - pytest.param({"key": {"key": 1}}, id="nested-dict"), ], ) - def test_expected_value_type_error(self, x): - with pytest.raises(TypeError): - approx(x) + def test_nonnumeric_okay_if_equal(self, x): + assert x == approx(x) + + @pytest.mark.parametrize( + "x", + [ + pytest.param("string"), + pytest.param(["string"], id="nested-str"), + pytest.param({"key": "string"}, id="dict-with-string"), + ], + ) + def test_nonnumeric_false_if_unequal(self, x): + """For nonnumeric types, x != pytest.approx(y) reduces to x != y""" + assert "ab" != approx("abc") + assert ["ab"] != approx(["abc"]) + # in particular, both of these should return False + assert {"a": 1.0} != approx({"a": None}) + assert {"a": None} != approx({"a": 1.0}) + + assert 1.0 != approx(None) + assert None != approx(1.0) # noqa: E711 + + assert 1.0 != approx([None]) + assert None != approx([1.0]) # noqa: E711 + + @pytest.mark.skipif(sys.version_info < (3, 7), reason="requires ordered dicts") + def test_nonnumeric_dict_repr(self): + """Dicts with non-numerics and infinites have no tolerances""" + x1 = {"foo": 1.0000005, "bar": None, "foobar": inf} + assert ( + repr(approx(x1)) + == "approx({'foo': 1.0000005 ± 1.0e-06, 'bar': None, 'foobar': inf})" + ) + + def test_nonnumeric_list_repr(self): + """Lists with non-numerics and infinites have no tolerances""" + x1 = [1.0000005, None, inf] + assert repr(approx(x1)) == "approx([1.0000005 ± 1.0e-06, None, inf])" @pytest.mark.parametrize( "op", From 4a9192f727ba936141b6a59f04f9abc14ca3ce4f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 28 Sep 2020 18:52:51 +0300 Subject: [PATCH 650/823] findpaths: fix regression causing incorrect rootdir to be determined When switching from py.path.local to pathlib (70f3ad1c1f31b35d4004f92), `local.parts(reverse=True)` was translated incorrectly, leading to the wrong rootdir being determined in some non-trivial cases where parent directories have config files as well. --- changelog/7807.bugfix.rst | 1 + src/_pytest/config/findpaths.py | 7 ++----- testing/test_config.py | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 changelog/7807.bugfix.rst diff --git a/changelog/7807.bugfix.rst b/changelog/7807.bugfix.rst new file mode 100644 index 00000000000..93f3e56dc11 --- /dev/null +++ b/changelog/7807.bugfix.rst @@ -0,0 +1 @@ +Fixed regression in pytest 6.1.0 causing incorrect rootdir to be determined in some non-trivial cases where parent directories have config files as well. diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index facf30a87a2..167b9e7a006 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -1,4 +1,3 @@ -import itertools import os from typing import Dict from typing import Iterable @@ -100,7 +99,7 @@ def locate_config( args = [Path.cwd()] for arg in args: argpath = absolutepath(arg) - for base in itertools.chain((argpath,), reversed(argpath.parents)): + for base in (argpath, *argpath.parents): for config_name in config_names: p = base / config_name if p.is_file(): @@ -184,9 +183,7 @@ def determine_setup( ancestor = get_common_ancestor(dirs) rootdir, inipath, inicfg = locate_config([ancestor]) if rootdir is None and rootdir_cmd_arg is None: - for possible_rootdir in itertools.chain( - (ancestor,), reversed(ancestor.parents) - ): + for possible_rootdir in (ancestor, *ancestor.parents): if (possible_rootdir / "setup.py").is_file(): rootdir = possible_rootdir break diff --git a/testing/test_config.py b/testing/test_config.py index 0cfd11fd525..89fbbf8c957 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1375,6 +1375,21 @@ def test_with_existing_file_in_subdir( assert rootpath == tmp_path assert inipath is None + def test_with_config_also_in_parent_directory( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: + """Regression test for #7807.""" + (tmp_path / "setup.cfg").write_text("[tool:pytest]\n", "utf-8") + (tmp_path / "myproject").mkdir() + (tmp_path / "myproject" / "setup.cfg").write_text("[tool:pytest]\n", "utf-8") + (tmp_path / "myproject" / "tests").mkdir() + monkeypatch.chdir(tmp_path / "myproject") + + rootpath, inipath, _ = determine_setup(None, ["tests/"]) + + assert rootpath == tmp_path / "myproject" + assert inipath == tmp_path / "myproject" / "setup.cfg" + class TestOverrideIniArgs: @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) From 875f226d3bd29d1fd714b3bdaaf0038690f9c530 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 27 Sep 2020 21:25:04 +0300 Subject: [PATCH 651/823] Smoke test pytest-rerunfailures --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 3e96ef49b97..c0e7409648a 100644 --- a/tox.ini +++ b/tox.ini @@ -130,6 +130,7 @@ deps = pytest-flakes pytest-html pytest-mock + pytest-rerunfailures pytest-sugar pytest-trio pytest-twisted @@ -143,6 +144,7 @@ commands = pytest --cov=. simple_integration.py pytest --ds=django_settings simple_integration.py pytest --html=simple.html simple_integration.py + pytest --reruns 5 simple_integration.py pytest pytest_anyio_integration.py pytest pytest_asyncio_integration.py pytest pytest_mock_integration.py From db08c7fbb049592ef81485ceb40d41e5a3265840 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 29 Sep 2020 13:11:47 +0300 Subject: [PATCH 652/823] pathlib: improve comments on commonpath and bestrelpath --- src/_pytest/pathlib.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 355281039fd..20516745db1 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -581,7 +581,10 @@ def absolutepath(path: Union[Path, str]) -> Path: def commonpath(path1: Path, path2: Path) -> Optional[Path]: """Return the common part shared with the other path, or None if there is - no common part.""" + no common part. + + If one path is relative and one is absolute, returns None. + """ try: return Path(os.path.commonpath((str(path1), str(path2)))) except ValueError: @@ -592,13 +595,17 @@ def bestrelpath(directory: Path, dest: Path) -> str: """Return a string which is a relative path from directory to dest such that directory/bestrelpath == dest. + The paths must be either both absolute or both relative. + If no such path can be determined, returns dest. """ if dest == directory: return os.curdir # Find the longest common directory. base = commonpath(directory, dest) - # Can be the case on Windows. + # Can be the case on Windows for two absolute paths on different drives. + # Can be the case for two relative paths without common prefix. + # Can be the case for a relative path and an absolute path. if not base: return str(dest) reldirectory = directory.relative_to(base) From 61f80a783a38441040c5999a4033af8004ee4c6b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 29 Sep 2020 09:29:27 +0300 Subject: [PATCH 653/823] terminal: fix crash in header reporting when absolute testpaths is used Regressed in 6.1.0 in 62e249a1f934d1073c9a0167077e133c5e0f6270. The `x` is an `str` but is expected to be a `pathlib.Path`. Not caught by mypy because `config.getini()` returns `Any`. Fix by just removing the `bestrelpath` call: - testpaths are always relative to the rootdir, it thus would be very unusual to specify an absolute path there. - The code was wrong even before the regression: `py.path.local`'s `bestrelpath` function expects a `py.path.local`, not an `str`. But it had some weird `try ... except AttributeError` fallback which just returns the argument, i.e. it was a no-op. So there is no behavior change. - It seems reasonable to me to just print the full path if that's what the ini specifies. --- changelog/7814.bugfix.rst | 1 + src/_pytest/terminal.py | 6 +++--- testing/test_terminal.py | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 changelog/7814.bugfix.rst diff --git a/changelog/7814.bugfix.rst b/changelog/7814.bugfix.rst new file mode 100644 index 00000000000..a5f2a9a9518 --- /dev/null +++ b/changelog/7814.bugfix.rst @@ -0,0 +1 @@ +Fixed crash in header reporting when :confval:`testpaths` is used and contains absolute paths (regression in 6.1.0). diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index e059612c212..34933ad2185 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -718,10 +718,10 @@ def pytest_report_header(self, config: Config) -> List[str]: if config.inipath: line += ", configfile: " + bestrelpath(config.rootpath, config.inipath) - testpaths = config.getini("testpaths") + testpaths = config.getini("testpaths") # type: List[str] if testpaths and config.args == testpaths: - rel_paths = [bestrelpath(config.rootpath, x) for x in testpaths] - line += ", testpaths: {}".format(", ".join(rel_paths)) + line += ", testpaths: {}".format(", ".join(testpaths)) + result = [line] plugininfo = config.pluginmanager.list_plugin_distinfo() diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 57db1b9a529..51fdf728ec9 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -18,6 +18,7 @@ from _pytest._io.wcwidth import wcswidth from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import Path from _pytest.pytester import Testdir from _pytest.reports import BaseReport @@ -749,6 +750,29 @@ def test_header(self, testdir): result = testdir.runpytest("tests") result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"]) + def test_header_absolute_testpath( + self, testdir: Testdir, monkeypatch: MonkeyPatch + ) -> None: + """Regresstion test for #7814.""" + tests = testdir.tmpdir.join("tests") + tests.ensure_dir() + testdir.makepyprojecttoml( + """ + [tool.pytest.ini_options] + testpaths = ['{}'] + """.format( + tests + ) + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "rootdir: *absolute_testpath0, configfile: pyproject.toml, testpaths: {}".format( + tests + ) + ] + ) + def test_no_header(self, testdir): testdir.tmpdir.join("tests").ensure_dir() testdir.tmpdir.join("gui").ensure_dir() From 3ecdad67b769c963fe08a7da41650d4a6262935c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 29 Sep 2020 13:25:34 +0300 Subject: [PATCH 654/823] terminal: improve condition on whether to display testpaths in header Make it match better the condition on whether testpaths is used (found in config/__init__.py). --- src/_pytest/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 34933ad2185..2e7ebded602 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -719,7 +719,7 @@ def pytest_report_header(self, config: Config) -> List[str]: line += ", configfile: " + bestrelpath(config.rootpath, config.inipath) testpaths = config.getini("testpaths") # type: List[str] - if testpaths and config.args == testpaths: + if config.invocation_params.dir == config.rootpath and config.args == testpaths: line += ", testpaths: {}".format(", ".join(testpaths)) result = [line] From cb0a13a523e7d9f1e9f3bd77095a9f58adb4e7b7 Mon Sep 17 00:00:00 2001 From: Max Voitko Date: Fri, 2 Oct 2020 16:39:15 +0300 Subject: [PATCH 655/823] Fix minor typos in doctest.rst (#7828) --- doc/en/doctest.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 1963214f7a4..5a3c76a126f 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -2,7 +2,7 @@ Doctest integration for modules and test files ========================================================= -By default all files matching the ``test*.txt`` pattern will +By default, all files matching the ``test*.txt`` pattern will be run through the python standard ``doctest`` module. You can change the pattern by issuing: @@ -206,7 +206,7 @@ It is possible to use fixtures using the ``getfixture`` helper: >>> ... >>> -Note that the fixture needs to be defined in a place visible by pytest, for example a `conftest.py` +Note that the fixture needs to be defined in a place visible by pytest, for example, a `conftest.py` file or plugin; normal python files containing docstrings are not normally scanned for fixtures unless explicitly configured by :confval:`python_files`. From 179f4326df2b644f0ab73f78e4770dafcbdcd89f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 12:03:23 -0700 Subject: [PATCH 656/823] py36+: drop python3.5 in CI and setup.cfg --- .github/workflows/main.yml | 11 ------- .travis.yml | 60 ------------------------------------- changelog/7808.breaking.rst | 1 + setup.cfg | 3 +- tox.ini | 1 - 5 files changed, 2 insertions(+), 74 deletions(-) delete mode 100644 .travis.yml create mode 100644 changelog/7808.breaking.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 84b7de5f8e8..4125557a8de 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,13 +21,11 @@ jobs: fail-fast: false matrix: name: [ - "windows-py35", "windows-py36", "windows-py37", "windows-py37-pluggy", "windows-py38", - "ubuntu-py35", "ubuntu-py36", "ubuntu-py37", "ubuntu-py37-pluggy", @@ -45,11 +43,6 @@ jobs: ] include: - - name: "windows-py35" - python: "3.5" - os: windows-latest - tox_env: "py35-xdist" - use_coverage: true - name: "windows-py36" python: "3.6" os: windows-latest @@ -68,10 +61,6 @@ jobs: tox_env: "py38-unittestextras" use_coverage: true - - name: "ubuntu-py35" - python: "3.5" - os: ubuntu-latest - tox_env: "py35-xdist" - name: "ubuntu-py36" python: "3.6" os: ubuntu-latest diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5c85dfe1f59..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,60 +0,0 @@ -language: python -dist: trusty -python: '3.5.1' -cache: false - -env: - global: - - PYTEST_ADDOPTS=-vv - -# setuptools-scm needs all tags in order to obtain a proper version -git: - depth: false - -install: - - python -m pip install --upgrade --pre tox - -jobs: - include: - # Coverage for Python 3.5.{0,1} specific code, mostly typing related. - - env: TOXENV=py35 PYTEST_COVERAGE=1 PYTEST_ADDOPTS="-k test_raises_cyclic_reference" - before_install: - # Work around https://github.com/jaraco/zipp/issues/40. - - python -m pip install -U 'setuptools>=34.4.0' virtualenv==16.7.9 - -before_script: - - | - # Do not (re-)upload coverage with cron runs. - if [[ "$TRAVIS_EVENT_TYPE" = cron ]]; then - PYTEST_COVERAGE=0 - fi - - | - if [[ "$PYTEST_COVERAGE" = 1 ]]; then - export COVERAGE_FILE="$PWD/.coverage" - export COVERAGE_PROCESS_START="$PWD/.coveragerc" - export _PYTEST_TOX_COVERAGE_RUN="coverage run -m" - export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess - fi - -script: tox - -after_success: - - | - if [[ "$PYTEST_COVERAGE" = 1 ]]; then - env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh -F Travis - fi - -notifications: - irc: - channels: - - "chat.freenode.net#pytest" - on_success: change - on_failure: change - skip_join: true - email: - - pytest-commit@python.org - -branches: - only: - - master - - /^\d+\.\d+\.x$/ diff --git a/changelog/7808.breaking.rst b/changelog/7808.breaking.rst new file mode 100644 index 00000000000..114b6a382cf --- /dev/null +++ b/changelog/7808.breaking.rst @@ -0,0 +1 @@ +pytest now supports python3.6+ only. diff --git a/setup.cfg b/setup.cfg index 6b54fc37096..64c64a607c1 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.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 @@ -50,7 +49,7 @@ install_requires = colorama;sys_platform=="win32" importlib-metadata>=0.12;python_version<"3.8" pathlib2>=2.2.0;python_version<"3.6" -python_requires = >=3.5 +python_requires = >=3.6 package_dir = =src setup_requires = diff --git a/tox.ini b/tox.ini index 3e96ef49b97..3ec0a87f874 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,6 @@ distshare = {homedir}/.tox/distshare # make sure to update environment list in travis.yml and appveyor.yml envlist = linting - py35 py36 py37 py38 From 3c93eb0f04a2c979b86fbc007c7c822abc97c3f6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 12:04:35 -0700 Subject: [PATCH 657/823] py36+: remove pathlib2 compatibility shim --- doc/en/builtin.rst | 4 ---- doc/en/tmpdir.rst | 2 +- setup.cfg | 1 - src/_pytest/pathlib.py | 7 ++----- src/_pytest/tmpdir.py | 4 ---- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 0fd58164c76..1d7fe76e3b7 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -169,10 +169,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a The returned object is a :class:`pathlib.Path` object. - .. note:: - - In python < 3.6 this is a pathlib2.Path. - no tests ran in 0.12s diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 5f882b1400f..a0d5cc0de0a 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -15,7 +15,7 @@ You can use the ``tmp_path`` fixture which will provide a temporary directory unique to the test invocation, created in the `base temporary directory`_. -``tmp_path`` is a ``pathlib/pathlib2.Path`` object. Here is an example test usage: +``tmp_path`` is a ``pathlib.Path`` object. Here is an example test usage: .. code-block:: python diff --git a/setup.cfg b/setup.cfg index 64c64a607c1..28ec061e028 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,7 +48,6 @@ install_requires = atomicwrites>=1.0;sys_platform=="win32" colorama;sys_platform=="win32" importlib-metadata>=0.12;python_version<"3.8" - pathlib2>=2.2.0;python_version<"3.6" python_requires = >=3.6 package_dir = =src diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 355281039fd..7179a786d04 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -14,6 +14,8 @@ from os.path import expandvars from os.path import isabs from os.path import sep +from pathlib import Path +from pathlib import PurePath from posixpath import sep as posix_sep from types import ModuleType from typing import Callable @@ -30,11 +32,6 @@ from _pytest.outcomes import skip from _pytest.warning_types import PytestWarning -if sys.version_info[:2] >= (3, 6): - from pathlib import Path, PurePath -else: - from pathlib2 import Path, PurePath - __all__ = ["Path", "PurePath"] diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index eb8aa9f9104..06bd764d450 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -190,10 +190,6 @@ def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path directory. The returned object is a :class:`pathlib.Path` object. - - .. note:: - - In python < 3.6 this is a pathlib2.Path. """ return _mk_tmp(request, tmp_path_factory) From 1f57fb079d40ac2580699eead4f873133a4137dd Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 12:06:21 -0700 Subject: [PATCH 658/823] py36+: remove _pytest.compat.MODULE_NOT_FOUND_ERROR --- src/_pytest/compat.py | 5 ----- testing/acceptance_test.py | 5 +---- testing/test_collection.py | 7 ++----- testing/test_doctest.py | 7 +++---- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 33221aac2d0..c28e904cbe8 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -46,11 +46,6 @@ class NotSetType(enum.Enum): NOTSET = NotSetType.token # type: Final # noqa: E305 # fmt: on -MODULE_NOT_FOUND_ERROR = ( - "ModuleNotFoundError" if sys.version_info[:2] >= (3, 6) else "ImportError" -) - - if sys.version_info >= (3, 8): from importlib import metadata as importlib_metadata else: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 039d8dad969..b3598901a6b 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -210,9 +210,6 @@ def foo(): """ ) result = testdir.runpytest() - exc_name = ( - "ModuleNotFoundError" if sys.version_info >= (3, 6) else "ImportError" - ) assert result.stdout.lines == [] assert result.stderr.lines == [ "ImportError while loading conftest '{}'.".format(conftest), @@ -220,7 +217,7 @@ def foo(): " foo()", "conftest.py:2: in foo", " import qwerty", - "E {}: No module named 'qwerty'".format(exc_name), + "E ModuleNotFoundError: No module named 'qwerty'", ] def test_early_skip(self, testdir): diff --git a/testing/test_collection.py b/testing/test_collection.py index 3e1b816b79e..3cb342a93d5 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1383,13 +1383,10 @@ def test_modules_not_importable_as_side_effect(self, testdir): """ self.setup_conftest_and_foo(testdir) result = testdir.runpytest("-v", "--import-mode=importlib") - exc_name = ( - "ModuleNotFoundError" if sys.version_info[:2] > (3, 5) else "ImportError" - ) result.stdout.fnmatch_lines( [ - "*{}: No module named 'foo'".format(exc_name), - "tests?test_foo.py:2: {}".format(exc_name), + "*ModuleNotFoundError: No module named 'foo'", + "tests?test_foo.py:2: ModuleNotFoundError", "* 1 failed in *", ] ) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 0b32ad32203..13b85979782 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -4,7 +4,6 @@ from typing import Optional import pytest -from _pytest.compat import MODULE_NOT_FOUND_ERROR from _pytest.doctest import _get_checker from _pytest.doctest import _is_mocked from _pytest.doctest import _is_setup_py @@ -399,8 +398,8 @@ def test_doctest_unex_importerror_only_txt(self, testdir): result.stdout.fnmatch_lines( [ "*>>> import asdals*", - "*UNEXPECTED*{e}*".format(e=MODULE_NOT_FOUND_ERROR), - "{e}: No module named *asdal*".format(e=MODULE_NOT_FOUND_ERROR), + "*UNEXPECTED*ModuleNotFoundError*", + "ModuleNotFoundError: No module named *asdal*", ] ) @@ -423,7 +422,7 @@ def test_doctest_unex_importerror_with_module(self, testdir): result.stdout.fnmatch_lines( [ "*ERROR collecting hello.py*", - "*{e}: No module named *asdals*".format(e=MODULE_NOT_FOUND_ERROR), + "*ModuleNotFoundError: No module named *asdals*", "*Interrupted: 1 error during collection*", ] ) From a238d1f37d3c953a2a1ff565e8475d1014441a94 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 12:09:56 -0700 Subject: [PATCH 659/823] py36+: remove TYPE_CHECKING from _pytest.compat automated with: ```bash git grep -l 'from .* import TYPE_CHECKING' | xargs reorder-python-imports \ --application-directories .:src \ --remove-import 'from _pytest.compat import TYPE_CHECKING' \ --add-import 'from typing import TYPE_CHECKING' ``` --- doc/en/conf.py | 2 +- src/_pytest/_code/code.py | 2 +- src/_pytest/assertion/__init__.py | 2 +- src/_pytest/assertion/rewrite.py | 2 +- src/_pytest/capture.py | 2 +- src/_pytest/compat.py | 7 +------ src/_pytest/config/__init__.py | 2 +- src/_pytest/config/argparsing.py | 2 +- src/_pytest/config/findpaths.py | 2 +- src/_pytest/debugging.py | 2 +- src/_pytest/doctest.py | 2 +- src/_pytest/fixtures.py | 2 +- src/_pytest/hookspec.py | 2 +- src/_pytest/main.py | 2 +- src/_pytest/mark/__init__.py | 2 +- src/_pytest/mark/expression.py | 3 +-- src/_pytest/mark/structures.py | 2 +- src/_pytest/nodes.py | 2 +- src/_pytest/pytester.py | 2 +- src/_pytest/python.py | 2 +- src/_pytest/python_api.py | 2 +- src/_pytest/recwarn.py | 2 +- src/_pytest/reports.py | 2 +- src/_pytest/runner.py | 2 +- src/_pytest/skipping.py | 2 +- src/_pytest/terminal.py | 2 +- src/_pytest/unittest.py | 2 +- src/_pytest/warning_types.py | 2 +- src/_pytest/warnings.py | 2 +- testing/code/test_excinfo.py | 2 +- testing/test_compat.py | 2 +- testing/test_config.py | 2 +- testing/test_junitxml.py | 2 +- testing/test_monkeypatch.py | 2 +- testing/test_runner.py | 2 +- 35 files changed, 35 insertions(+), 41 deletions(-) diff --git a/doc/en/conf.py b/doc/en/conf.py index c631484aa34..7e96c5de9fa 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -17,9 +17,9 @@ # The short X.Y version. import os import sys +from typing import TYPE_CHECKING from _pytest import __version__ as version -from _pytest.compat import TYPE_CHECKING if TYPE_CHECKING: import sphinx.application diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 6b908bb51e9..b48cac63247 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -21,6 +21,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union from weakref import ref @@ -40,7 +41,6 @@ from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.compat import overload -from _pytest.compat import TYPE_CHECKING from _pytest.pathlib import Path if TYPE_CHECKING: diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 06057d0c4c1..554ac191d6a 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -4,12 +4,12 @@ from typing import Generator from typing import List from typing import Optional +from typing import TYPE_CHECKING from _pytest.assertion import rewrite from _pytest.assertion import truncate from _pytest.assertion import util from _pytest.assertion.rewrite import assertstate_key -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 5ff57824579..0b737fc7810 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -22,6 +22,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import py @@ -33,7 +34,6 @@ format_explanation as _format_explanation, ) from _pytest.compat import fspath -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.main import Session from _pytest.pathlib import fnmatch_ex diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 2d2b392aba8..1b21da13721 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -14,11 +14,11 @@ from typing import Optional from typing import TextIO from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import pytest from _pytest.compat import final -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.fixtures import SubRequest diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 33221aac2d0..7d8a70fd4c2 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -14,6 +14,7 @@ from typing import Optional from typing import overload as overload from typing import Tuple +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -22,12 +23,6 @@ from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME -if sys.version_info < (3, 5, 2): - TYPE_CHECKING = False # type: bool -else: - from typing import TYPE_CHECKING - - if TYPE_CHECKING: from typing import NoReturn from typing import Type diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index f89ed37027b..9df2a355d63 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -26,6 +26,7 @@ from typing import Set from typing import TextIO from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import attr @@ -45,7 +46,6 @@ from _pytest._io import TerminalWriter from _pytest.compat import final from _pytest.compat import importlib_metadata -from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import fail from _pytest.outcomes import Skipped from _pytest.pathlib import bestrelpath diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 636021df455..16777587e21 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -11,13 +11,13 @@ from typing import Optional from typing import Sequence from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import py import _pytest._io from _pytest.compat import final -from _pytest.compat import TYPE_CHECKING from _pytest.config.exceptions import UsageError if TYPE_CHECKING: diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 167b9e7a006..ad2799777c4 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -5,12 +5,12 @@ from typing import Optional from typing import Sequence from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import iniconfig from .exceptions import UsageError -from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import fail from _pytest.pathlib import absolutepath from _pytest.pathlib import commonpath diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 6f641fb2d96..0fec6d817e3 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -9,11 +9,11 @@ from typing import List from typing import Optional from typing import Tuple +from typing import TYPE_CHECKING from typing import Union from _pytest import outcomes from _pytest._code import ExceptionInfo -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.config import hookimpl diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index c744bb369ea..304d3d9047b 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -17,6 +17,7 @@ from typing import Pattern from typing import Sequence from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import py.path @@ -28,7 +29,6 @@ from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter from _pytest.compat import safe_getattr -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureRequest diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f526f484b29..62def5a3319 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -19,6 +19,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -43,7 +44,6 @@ from _pytest.compat import order_preserving_dict from _pytest.compat import overload from _pytest.compat import safe_getattr -from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config.argparsing import Parser diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 2f0a04a06a7..aa0b5cef4a6 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -7,12 +7,12 @@ from typing import Optional from typing import Sequence from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import py.path from pluggy import HookspecMarker -from _pytest.compat import TYPE_CHECKING from _pytest.deprecated import WARNING_CAPTURED_HOOK if TYPE_CHECKING: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index ef106c46a43..a8eaf02592f 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -14,6 +14,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import attr @@ -23,7 +24,6 @@ from _pytest import nodes from _pytest.compat import final from _pytest.compat import overload -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import directory_arg from _pytest.config import ExitCode diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 6a9b262307e..94578e5289f 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -4,6 +4,7 @@ from typing import AbstractSet from typing import List from typing import Optional +from typing import TYPE_CHECKING from typing import Union import attr @@ -17,7 +18,6 @@ from .structures import MarkDecorator from .structures import MarkGenerator from .structures import ParameterSet -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index f5700109757..9f4592221b9 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -23,11 +23,10 @@ from typing import Mapping from typing import Optional from typing import Sequence +from typing import TYPE_CHECKING import attr -from _pytest.compat import TYPE_CHECKING - if TYPE_CHECKING: from typing import NoReturn diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 39a2321b3ff..989e2735116 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -13,6 +13,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -24,7 +25,6 @@ from ..compat import NOTSET from ..compat import NotSetType from ..compat import overload -from ..compat import TYPE_CHECKING from _pytest.config import Config from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 3665d8d5ef4..f8dbf826373 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -10,6 +10,7 @@ from typing import Optional from typing import Set from typing import Tuple +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -21,7 +22,6 @@ from _pytest._code.code import TerminalRepr from _pytest.compat import cached_property from _pytest.compat import overload -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d78062a86ce..7c7ac9348d6 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -18,6 +18,7 @@ from typing import Optional from typing import Sequence from typing import Tuple +from typing import TYPE_CHECKING from typing import Union from weakref import WeakKeyDictionary @@ -30,7 +31,6 @@ from _pytest.capture import _get_multicapture from _pytest.compat import final from _pytest.compat import overload -from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 7d3e301c076..61f5806ad4f 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -23,6 +23,7 @@ from typing import Optional from typing import Set from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import py @@ -49,7 +50,6 @@ from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass from _pytest.compat import STRING_TYPES -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 681f83028d8..e6d1f8cc134 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -13,6 +13,7 @@ from typing import Optional from typing import Pattern from typing import Tuple +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -20,7 +21,6 @@ from _pytest.compat import final from _pytest.compat import overload from _pytest.compat import STRING_TYPES -from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import fail if TYPE_CHECKING: diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 39d6de91455..b44852c80b6 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -10,12 +10,12 @@ from typing import Optional from typing import Pattern from typing import Tuple +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union from _pytest.compat import final from _pytest.compat import overload -from _pytest.compat import TYPE_CHECKING from _pytest.fixtures import fixture from _pytest.outcomes import fail diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index c42f778ec40..94c78581254 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -8,6 +8,7 @@ from typing import List from typing import Optional from typing import Tuple +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -27,7 +28,6 @@ from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter from _pytest.compat import final -from _pytest.compat import TYPE_CHECKING 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 f29d356fe07..74692e21145 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -9,6 +9,7 @@ from typing import List from typing import Optional from typing import Tuple +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -23,7 +24,6 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest.compat import final -from _pytest.compat import TYPE_CHECKING from _pytest.config.argparsing import Parser from _pytest.nodes import Collector from _pytest.nodes import Item diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index c5b4ff39e85..9be49bbfad4 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -6,10 +6,10 @@ from typing import Generator from typing import Optional from typing import Tuple +from typing import TYPE_CHECKING import attr -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index e059612c212..727c15cb879 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -20,6 +20,7 @@ from typing import Set from typing import TextIO from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import attr @@ -34,7 +35,6 @@ from _pytest._io.wcwidth import wcswidth from _pytest.compat import final from _pytest.compat import order_preserving_dict -from _pytest.compat import TYPE_CHECKING from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 09aa014c58a..6b466da96bf 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -9,13 +9,13 @@ from typing import List from typing import Optional from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import _pytest._code import pytest from _pytest.compat import getimfunc from _pytest.compat import is_async_function -from _pytest.compat import TYPE_CHECKING from _pytest.config import hookimpl from _pytest.fixtures import FixtureRequest from _pytest.nodes import Collector diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 52e4d2b14cb..a7490dc776a 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -1,11 +1,11 @@ from typing import Any from typing import Generic +from typing import TYPE_CHECKING from typing import TypeVar import attr from _pytest.compat import final -from _pytest.compat import TYPE_CHECKING if TYPE_CHECKING: from typing import Type # noqa: F401 (used in type string) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 950d0bb3859..35eed96df58 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -3,9 +3,9 @@ from contextlib import contextmanager from typing import Generator from typing import Optional +from typing import TYPE_CHECKING import pytest -from _pytest.compat import TYPE_CHECKING from _pytest.config import apply_warning_filters from _pytest.config import Config from _pytest.config import parse_warning_filter diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 5754977ddc7..ce07d4dbcce 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -7,6 +7,7 @@ from typing import Any from typing import Dict from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import py @@ -17,7 +18,6 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import FormattedExcinfo from _pytest._io import TerminalWriter -from _pytest.compat import TYPE_CHECKING from _pytest.pytester import LineMatcher try: diff --git a/testing/test_compat.py b/testing/test_compat.py index 5debe87a3ed..86b8fff9f57 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -2,6 +2,7 @@ import sys from functools import partial from functools import wraps +from typing import TYPE_CHECKING from typing import Union import pytest @@ -12,7 +13,6 @@ from _pytest.compat import is_generator from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass -from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import OutcomeException if TYPE_CHECKING: diff --git a/testing/test_config.py b/testing/test_config.py index 89fbbf8c957..02db737d6b9 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -6,6 +6,7 @@ from typing import List from typing import Sequence from typing import Tuple +from typing import TYPE_CHECKING import attr import py.path @@ -13,7 +14,6 @@ import _pytest._code import pytest from _pytest.compat import importlib_metadata -from _pytest.compat import TYPE_CHECKING from _pytest.config import _get_plugin_specs_as_list from _pytest.config import _iter_rewritable_modules from _pytest.config import _strtobool diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 3cc93a39805..5a5610a6078 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -4,13 +4,13 @@ from typing import cast from typing import List from typing import Tuple +from typing import TYPE_CHECKING from xml.dom import minidom import py import xmlschema import pytest -from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.junitxml import bin_xml_escape from _pytest.junitxml import LogXML diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index fea8a28fba8..52636f85123 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -4,11 +4,11 @@ import textwrap from typing import Dict from typing import Generator +from typing import TYPE_CHECKING import py import pytest -from _pytest.compat import TYPE_CHECKING from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Testdir diff --git a/testing/test_runner.py b/testing/test_runner.py index b9d22370a7b..dff555534b7 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -5,6 +5,7 @@ from typing import Dict from typing import List from typing import Tuple +from typing import TYPE_CHECKING import py @@ -13,7 +14,6 @@ from _pytest import outcomes from _pytest import reports from _pytest import runner -from _pytest.compat import TYPE_CHECKING from _pytest.config import ExitCode from _pytest.outcomes import OutcomeException From 284fd45a086f3943b616fed1e608a3109d372c3b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 12:13:44 -0700 Subject: [PATCH 660/823] py36+: miscellaneous (3, 6) cleanup --- doc/en/skipping.rst | 2 +- src/_pytest/capture.py | 6 +----- src/_pytest/compat.py | 4 +--- testing/acceptance_test.py | 3 --- testing/test_capture.py | 3 +-- testing/test_compat.py | 4 ---- 6 files changed, 4 insertions(+), 18 deletions(-) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 5c67d77a7ec..c463f3293bc 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -91,7 +91,7 @@ when run on an interpreter earlier than Python3.6: import sys - @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher") + @pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_function(): ... diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 2d2b392aba8..629e3c09f86 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -113,11 +113,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: See https://github.com/pytest-dev/py/issues/103. """ - if ( - not sys.platform.startswith("win32") - or sys.version_info[:2] < (3, 6) - or hasattr(sys, "pypy_version_info") - ): + if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"): return # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666). diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 33221aac2d0..4bb616785ba 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -97,9 +97,7 @@ def syntax, and doesn't contain yield), or a function decorated with def is_async_function(func: object) -> bool: """Return True if the given function seems to be an async function or an async generator.""" - return iscoroutinefunction(func) or ( - sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func) - ) + return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) def getlocation(function, curdir: Optional[str] = None) -> str: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 039d8dad969..01fe97d4f6c 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1183,9 +1183,6 @@ def test_3(): @pytest.mark.filterwarnings("default") -@pytest.mark.skipif( - sys.version_info < (3, 6), reason="async gen syntax available in Python 3.6+" -) def test_warn_on_async_gen_function(testdir): testdir.makepyfile( test_async=""" diff --git a/testing/test_capture.py b/testing/test_capture.py index 5f820d8465b..317a5922741 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1421,8 +1421,7 @@ def test_capattr(): @pytest.mark.skipif( - not sys.platform.startswith("win") and sys.version_info[:2] >= (3, 6), - reason="only py3.6+ on windows", + not sys.platform.startswith("win"), reason="only on windows", ) def test_py36_windowsconsoleio_workaround_non_standard_streams() -> None: """ diff --git a/testing/test_compat.py b/testing/test_compat.py index 5debe87a3ed..34c8495d29c 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -1,5 +1,4 @@ import enum -import sys from functools import partial from functools import wraps from typing import Union @@ -129,9 +128,6 @@ async def bar(): result.stdout.fnmatch_lines(["*1 passed*"]) -@pytest.mark.skipif( - sys.version_info < (3, 6), reason="async gen syntax available in Python 3.6+" -) def test_is_generator_async_gen_syntax(testdir): testdir.makepyfile( """ From cf220b92a29630fd8707e2cefb3f96006a9bb4d3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 12:31:28 -0700 Subject: [PATCH 661/823] py36+: replace typing.X with X --- src/_pytest/mark/__init__.py | 4 ++-- src/_pytest/mark/structures.py | 6 +++--- src/_pytest/python.py | 31 +++++++++++++------------------ 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 6a9b262307e..dcc467b0ffa 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -1,7 +1,7 @@ """Generic mechanism for marking and selecting python functions.""" -import typing import warnings from typing import AbstractSet +from typing import Collection from typing import List from typing import Optional from typing import Union @@ -46,7 +46,7 @@ def param( *values: object, - marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (), + marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (), id: Optional[str] = None ) -> ParameterSet: """Specify a parameter in `pytest.mark.parametrize`_ calls or diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 39a2321b3ff..0e566f9eda2 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -1,9 +1,9 @@ import collections.abc import inspect -import typing import warnings from typing import Any from typing import Callable +from typing import Collection from typing import Iterable from typing import Iterator from typing import List @@ -79,7 +79,7 @@ class ParameterSet( "ParameterSet", [ ("values", Sequence[Union[object, NotSetType]]), - ("marks", "typing.Collection[Union[MarkDecorator, Mark]]"), + ("marks", Collection[Union["MarkDecorator", "Mark"]]), ("id", Optional[str]), ], ) @@ -88,7 +88,7 @@ class ParameterSet( def param( cls, *values: object, - marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (), + marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (), id: Optional[str] = None ) -> "ParameterSet": if isinstance(marks, MarkDecorator): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 7d3e301c076..c66fa480460 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -6,11 +6,9 @@ import os import sys import types -import typing import warnings from collections import Counter from collections import defaultdict -from collections.abc import Sequence from functools import partial from typing import Any from typing import Callable @@ -21,6 +19,7 @@ from typing import List from typing import Mapping from typing import Optional +from typing import Sequence from typing import Set from typing import Tuple from typing import Union @@ -668,7 +667,7 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: def _collectfile( self, path: py.path.local, handle_dupes: bool = True - ) -> typing.Sequence[nodes.Collector]: + ) -> Sequence[nodes.Collector]: assert ( path.isfile() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( @@ -904,7 +903,7 @@ def id(self) -> str: def setmulti2( self, valtypes: Mapping[str, "Literal['params', 'funcargs']"], - argnames: typing.Sequence[str], + argnames: Sequence[str], valset: Iterable[object], id: str, marks: Iterable[Union[Mark, MarkDecorator]], @@ -966,8 +965,8 @@ def __init__( def parametrize( self, argnames: Union[str, List[str], Tuple[str, ...]], - argvalues: Iterable[Union[ParameterSet, typing.Sequence[object], object]], - indirect: Union[bool, typing.Sequence[str]] = False, + argvalues: Iterable[Union[ParameterSet, Sequence[object], object]], + indirect: Union[bool, Sequence[str]] = False, ids: Optional[ Union[ Iterable[Union[None, str, float, int, bool]], @@ -1093,14 +1092,14 @@ def parametrize( def _resolve_arg_ids( self, - argnames: typing.Sequence[str], + argnames: Sequence[str], ids: Optional[ Union[ Iterable[Union[None, str, float, int, bool]], Callable[[Any], Optional[object]], ] ], - parameters: typing.Sequence[ParameterSet], + parameters: Sequence[ParameterSet], nodeid: str, ) -> List[str]: """Resolve the actual ids for the given argnames, based on the ``ids`` parameter given @@ -1127,7 +1126,7 @@ def _resolve_arg_ids( def _validate_ids( self, ids: Iterable[Union[None, str, float, int, bool]], - parameters: typing.Sequence[ParameterSet], + parameters: Sequence[ParameterSet], func_name: str, ) -> List[Union[None, str]]: try: @@ -1162,9 +1161,7 @@ def _validate_ids( return new_ids def _resolve_arg_value_types( - self, - argnames: typing.Sequence[str], - indirect: Union[bool, typing.Sequence[str]], + self, argnames: Sequence[str], indirect: Union[bool, Sequence[str]], ) -> Dict[str, "Literal['params', 'funcargs']"]: """Resolve if each parametrized argument must be considered a parameter to a fixture or a "funcarg" to the function, based on the @@ -1202,9 +1199,7 @@ def _resolve_arg_value_types( return valtypes def _validate_if_using_arg_names( - self, - argnames: typing.Sequence[str], - indirect: Union[bool, typing.Sequence[str]], + self, argnames: Sequence[str], indirect: Union[bool, Sequence[str]], ) -> None: """Check if all argnames are being used, by default values, or directly/indirectly. @@ -1235,9 +1230,9 @@ def _validate_if_using_arg_names( def _find_parametrized_scope( - argnames: typing.Sequence[str], - arg2fixturedefs: Mapping[str, typing.Sequence[fixtures.FixtureDef[object]]], - indirect: Union[bool, typing.Sequence[str]], + argnames: Sequence[str], + arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]], + indirect: Union[bool, Sequence[str]], ) -> "fixtures._Scope": """Find the most appropriate scope for a parametrized call based on its arguments. From e622cb7c4122abcf38a6482b1ef54b5c6cdb936a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 13:05:29 -0700 Subject: [PATCH 662/823] py36+: remove workaround for Union[Pattern/Match] bug --- src/_pytest/_code/code.py | 2 +- src/_pytest/python_api.py | 4 ++-- src/_pytest/recwarn.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 6b908bb51e9..595686c6926 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -625,7 +625,7 @@ def getrepr( ) return fmt.repr_excinfo(self) - def match(self, regexp: "Union[str, Pattern[str]]") -> "Literal[True]": + def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]": """Check whether the regular expression `regexp` matches the string representation of the exception using :func:`python:re.search`. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 681f83028d8..0a97ad393f9 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -562,7 +562,7 @@ def _is_numpy_array(obj: object) -> bool: def raises( expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], *, - match: "Optional[Union[str, Pattern[str]]]" = ... + match: Optional[Union[str, Pattern[str]]] = ... ) -> "RaisesContext[_E]": ... @@ -740,7 +740,7 @@ def __init__( self, expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], message: str, - match_expr: Optional[Union[str, "Pattern[str]"]] = None, + match_expr: Optional[Union[str, Pattern[str]]] = None, ) -> None: self.expected_exception = expected_exception self.message = message diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 39d6de91455..17d169a7764 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -41,7 +41,7 @@ def recwarn() -> Generator["WarningsRecorder", None, None]: @overload def deprecated_call( - *, match: Optional[Union[str, "Pattern[str]"]] = ... + *, match: Optional[Union[str, Pattern[str]]] = ... ) -> "WarningsRecorder": ... @@ -88,7 +88,7 @@ def deprecated_call( # noqa: F811 def warns( expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]], *, - match: "Optional[Union[str, Pattern[str]]]" = ... + match: Optional[Union[str, Pattern[str]]] = ... ) -> "WarningsChecker": ... @@ -106,7 +106,7 @@ def warns( # noqa: F811 def warns( # noqa: F811 expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]], *args: Any, - match: Optional[Union[str, "Pattern[str]"]] = None, + match: Optional[Union[str, Pattern[str]]] = None, **kwargs: Any ) -> Union["WarningsChecker", Any]: r"""Assert that code raises a particular class of warning. @@ -236,7 +236,7 @@ def __init__( expected_warning: Optional[ Union["Type[Warning]", Tuple["Type[Warning]", ...]] ] = None, - match_expr: Optional[Union[str, "Pattern[str]"]] = None, + match_expr: Optional[Union[str, Pattern[str]]] = None, ) -> None: super().__init__() From daba7ceb7116b2f402f01f80814c833b5a8370db Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 13:23:41 -0700 Subject: [PATCH 663/823] py36+: remove requires_ordered_markup --- testing/conftest.py | 22 ---------------------- testing/test_terminal.py | 3 --- 2 files changed, 25 deletions(-) diff --git a/testing/conftest.py b/testing/conftest.py index a667be42fcb..62a2b4a22b8 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -3,7 +3,6 @@ from typing import List import pytest -from _pytest.pytester import RunResult from _pytest.pytester import Testdir if sys.gettrace(): @@ -175,27 +174,6 @@ def format_for_rematch(cls, lines: List[str]) -> List[str]: """Replace color names for use with LineMatcher.re_match_lines""" return [line.format(**cls.RE_COLORS) for line in lines] - @classmethod - def requires_ordered_markup(cls, result: RunResult): - """Should be called if a test expects markup to appear in the output - in the order they were passed, for example: - - tw.write(line, bold=True, red=True) - - In Python 3.5 there's no guarantee that the generated markup will appear - in the order called, so we do some limited color testing and skip the rest of - the test. - """ - if sys.version_info < (3, 6): - # terminal writer.write accepts keyword arguments, so - # py36+ is required so the markup appears in the expected order - output = result.stdout.str() - assert "test session starts" in output - assert "\x1b[1m" in output - pytest.skip( - "doing limited testing because lacking ordered markup on py35" - ) - return ColorMapping diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 57db1b9a529..859e1470e46 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1017,7 +1017,6 @@ def test_this(): """ ) result = testdir.runpytest("--color=yes", str(p1)) - color_mapping.requires_ordered_markup(result) result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ @@ -2217,7 +2216,6 @@ def test_foo(): """ ) result = testdir.runpytest("--color=yes") - color_mapping.requires_ordered_markup(result) result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ @@ -2237,7 +2235,6 @@ def test_foo(): """ ) result = testdir.runpytest("--color=yes") - color_mapping.requires_ordered_markup(result) result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( From 6ee1eadd1c91e77b07d2275faae30398d5cb6b19 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 2 Oct 2020 23:40:50 -0300 Subject: [PATCH 664/823] Fake setuptools-scm into using version 6.2.0a1 Due to pytest-rerunfailures latest version requiring 6.1.0, which is not tagged on master. --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index c0e7409648a..a4c311b37b6 100644 --- a/tox.ini +++ b/tox.ini @@ -138,6 +138,8 @@ deps = pytest-xvfb setenv = PYTHONPATH=. + # due to pytest-rerunfailures requiring 6.2+; can be removed after 6.2.0 + SETUPTOOLS_SCM_PRETEND_VERSION=6.2.0a1 commands = pip check pytest bdd_wallet.py From be43c7c67baf42ef4b2beb13cd8bb26c02094241 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 12:20:51 -0700 Subject: [PATCH 665/823] py36+: remove _pytest.compat.fspath --- src/_pytest/assertion/rewrite.py | 15 +++++++-------- src/_pytest/compat.py | 13 ------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 0b737fc7810..28a206c27a3 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -33,7 +33,6 @@ from _pytest.assertion.util import ( # noqa: F401 format_explanation as _format_explanation, ) -from _pytest.compat import fspath from _pytest.config import Config from _pytest.main import Session from _pytest.pathlib import fnmatch_ex @@ -306,7 +305,7 @@ def _write_pyc( pyc: Path, ) -> bool: try: - with atomic_write(fspath(pyc), mode="wb", overwrite=True) as fp: + with atomic_write(os.fspath(pyc), mode="wb", overwrite=True) as fp: _write_pyc_fp(fp, source_stat, co) except OSError as e: state.trace("error writing pyc file at {}: {}".format(pyc, e)) @@ -336,7 +335,7 @@ def _write_pyc( try: _write_pyc_fp(fp, source_stat, co) - os.rename(proc_pyc, fspath(pyc)) + os.rename(proc_pyc, os.fspath(pyc)) except OSError as e: state.trace("error writing pyc file at {}: {}".format(pyc, e)) # we ignore any failure to write the cache file @@ -350,7 +349,7 @@ def _write_pyc( def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]: """Read and rewrite *fn* and return the code object.""" - fn_ = fspath(fn) + fn_ = os.fspath(fn) stat = os.stat(fn_) with open(fn_, "rb") as f: source = f.read() @@ -368,12 +367,12 @@ def _read_pyc( Return rewritten code if successful or None if not. """ try: - fp = open(fspath(pyc), "rb") + fp = open(os.fspath(pyc), "rb") except OSError: return None with fp: try: - stat_result = os.stat(fspath(source)) + stat_result = os.stat(os.fspath(source)) mtime = int(stat_result.st_mtime) size = stat_result.st_size data = fp.read(12) @@ -831,7 +830,7 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]: "assertion is always true, perhaps remove parentheses?" ), category=None, - filename=fspath(self.module_path), + filename=os.fspath(self.module_path), lineno=assert_.lineno, ) @@ -1072,7 +1071,7 @@ def try_makedirs(cache_dir: Path) -> bool: Returns True if successful or if it already exists. """ try: - os.makedirs(fspath(cache_dir), exist_ok=True) + os.makedirs(os.fspath(cache_dir), exist_ok=True) except (FileNotFoundError, NotADirectoryError, FileExistsError): # One of the path components was not a directory: # - we're in a zip file diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 49b00c58e3b..cbfeb96bcf9 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -2,7 +2,6 @@ import enum import functools import inspect -import os import re import sys from contextlib import contextmanager @@ -55,18 +54,6 @@ def _format_args(func: Callable[..., Any]) -> str: REGEX_TYPE = type(re.compile("")) -if sys.version_info < (3, 6): - - def fspath(p): - """os.fspath replacement, useful to point out when we should replace it by the - real function once we drop py35.""" - return str(p) - - -else: - fspath = os.fspath - - def is_generator(func: object) -> bool: genfunc = inspect.isgeneratorfunction(func) return genfunc and not iscoroutinefunction(func) From bfadd4060e83a7c8dc6233643ad9fa4af7368806 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 13:02:22 -0700 Subject: [PATCH 666/823] py36+: from typing import Type: no longer need guard --- src/_pytest/_code/code.py | 12 ++++++------ src/_pytest/compat.py | 6 +++--- src/_pytest/config/__init__.py | 10 ++++------ src/_pytest/debugging.py | 3 +-- src/_pytest/doctest.py | 8 ++++---- src/_pytest/fixtures.py | 4 ++-- src/_pytest/main.py | 2 +- src/_pytest/mark/structures.py | 7 ++----- src/_pytest/nodes.py | 5 ++--- src/_pytest/outcomes.py | 4 ++-- src/_pytest/pytester.py | 4 ++-- src/_pytest/python.py | 2 +- src/_pytest/python_api.py | 21 +++++++-------------- src/_pytest/recwarn.py | 19 ++++++++----------- src/_pytest/reports.py | 6 +++--- src/_pytest/runner.py | 6 ++++-- src/_pytest/skipping.py | 7 ++----- src/_pytest/unittest.py | 2 +- src/_pytest/warning_types.py | 7 ++----- testing/test_config.py | 7 ++----- testing/test_monkeypatch.py | 7 ++----- testing/test_runner.py | 7 ++----- 22 files changed, 63 insertions(+), 93 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 65b2bd63823..ce63b4b16bf 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -21,6 +21,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -44,7 +45,6 @@ from _pytest.pathlib import Path if TYPE_CHECKING: - from typing import Type from typing_extensions import Literal from weakref import ReferenceType @@ -421,14 +421,14 @@ class ExceptionInfo(Generic[_E]): _assert_start_repr = "AssertionError('assert " - _excinfo = attr.ib(type=Optional[Tuple["Type[_E]", "_E", TracebackType]]) + _excinfo = attr.ib(type=Optional[Tuple[Type["_E"], "_E", TracebackType]]) _striptext = attr.ib(type=str, default="") _traceback = attr.ib(type=Optional[Traceback], default=None) @classmethod def from_exc_info( cls, - exc_info: Tuple["Type[_E]", "_E", TracebackType], + exc_info: Tuple[Type[_E], _E, TracebackType], exprinfo: Optional[str] = None, ) -> "ExceptionInfo[_E]": """Return an ExceptionInfo for an existing exc_info tuple. @@ -479,13 +479,13 @@ def for_later(cls) -> "ExceptionInfo[_E]": """Return an unfilled ExceptionInfo.""" return cls(None) - def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None: + def fill_unfilled(self, exc_info: Tuple[Type[_E], _E, TracebackType]) -> None: """Fill an unfilled ExceptionInfo created with ``for_later()``.""" assert self._excinfo is None, "ExceptionInfo was already filled" self._excinfo = exc_info @property - def type(self) -> "Type[_E]": + def type(self) -> Type[_E]: """The exception class.""" assert ( self._excinfo is not None @@ -551,7 +551,7 @@ def exconly(self, tryshort: bool = False) -> str: return text def errisinstance( - self, exc: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]] + self, exc: Union[Type[BaseException], Tuple[Type[BaseException], ...]] ) -> bool: """Return True if the exception is an instance of exc. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 49b00c58e3b..c5de190e3d6 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -25,7 +25,6 @@ if TYPE_CHECKING: from typing import NoReturn - from typing import Type from typing_extensions import Final @@ -362,6 +361,7 @@ def final(f): # noqa: F811 if sys.version_info >= (3, 8): from functools import cached_property as cached_property else: + from typing import Type class cached_property(Generic[_S, _T]): __slots__ = ("func", "__doc__") @@ -372,13 +372,13 @@ def __init__(self, func: Callable[[_S], _T]) -> None: @overload def __get__( - self, instance: None, owner: Optional["Type[_S]"] = ... + self, instance: None, owner: Optional[Type[_S]] = ... ) -> "cached_property[_S, _T]": ... @overload # noqa: F811 def __get__( # noqa: F811 - self, instance: _S, owner: Optional["Type[_S]"] = ... + self, instance: _S, owner: Optional[Type[_S]] = ... ) -> _T: ... diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 9df2a355d63..799cc19c613 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -26,6 +26,7 @@ from typing import Set from typing import TextIO from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import Union @@ -56,7 +57,6 @@ from _pytest.warning_types import PytestConfigWarning if TYPE_CHECKING: - from typing import Type from _pytest._code.code import _TracebackStyle from _pytest.terminal import TerminalReporter @@ -104,7 +104,7 @@ class ConftestImportFailure(Exception): def __init__( self, path: py.path.local, - excinfo: Tuple["Type[Exception]", Exception, TracebackType], + excinfo: Tuple[Type[Exception], Exception, TracebackType], ) -> None: super().__init__(path, excinfo) self.path = path @@ -1560,7 +1560,7 @@ def _strtobool(val: str) -> bool: @lru_cache(maxsize=50) def parse_warning_filter( arg: str, *, escape: bool -) -> "Tuple[str, str, Type[Warning], str, int]": +) -> Tuple[str, str, Type[Warning], str, int]: """Parse a warnings filter string. This is copied from warnings._setoption, but does not apply the filter, @@ -1573,9 +1573,7 @@ def parse_warning_filter( parts.append("") action_, message, category_, module, lineno_ = [s.strip() for s in parts] action = warnings._getaction(action_) # type: str # type: ignore[attr-defined] - category = warnings._getcategory( - category_ - ) # type: Type[Warning] # type: ignore[attr-defined] + category: Type[Warning] = warnings._getcategory(category_) # type: ignore[attr-defined] if message and escape: message = re.escape(message) if module and escape: diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 0fec6d817e3..2b3faf8dc6c 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -9,6 +9,7 @@ from typing import List from typing import Optional from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import Union @@ -24,8 +25,6 @@ from _pytest.reports import BaseReport if TYPE_CHECKING: - from typing import Type - from _pytest.capture import CaptureManager from _pytest.runner import CallInfo diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 304d3d9047b..817c9f9a841 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -17,6 +17,7 @@ from typing import Pattern from typing import Sequence from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import Union @@ -40,7 +41,6 @@ if TYPE_CHECKING: import doctest - from typing import Type DOCTEST_REPORT_CHOICE_NONE = "none" DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" @@ -168,7 +168,7 @@ def __init__(self, failures: "Sequence[doctest.DocTestFailure]") -> None: self.failures = failures -def _init_runner_class() -> "Type[doctest.DocTestRunner]": +def _init_runner_class() -> Type["doctest.DocTestRunner"]: import doctest class PytestDoctestRunner(doctest.DebugRunner): @@ -204,7 +204,7 @@ def report_unexpected_exception( out, test: "doctest.DocTest", example: "doctest.Example", - exc_info: "Tuple[Type[BaseException], BaseException, types.TracebackType]", + exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType], ) -> None: if isinstance(exc_info[1], OutcomeException): raise exc_info[1] @@ -568,7 +568,7 @@ def func() -> None: return fixture_request -def _init_checker_class() -> "Type[doctest.OutputChecker]": +def _init_checker_class() -> Type["doctest.OutputChecker"]: import doctest import re diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 62def5a3319..d793ea37f1e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -19,6 +19,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -57,7 +58,6 @@ if TYPE_CHECKING: from typing import Deque from typing import NoReturn - from typing import Type from typing_extensions import Literal from _pytest import nodes @@ -91,7 +91,7 @@ # Cache key. object, # Exc info if raised. - Tuple["Type[BaseException]", BaseException, TracebackType], + Tuple[Type[BaseException], BaseException, TracebackType], ], ] diff --git a/src/_pytest/main.py b/src/_pytest/main.py index a8eaf02592f..31dc28e5d0e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -14,6 +14,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import Union @@ -44,7 +45,6 @@ if TYPE_CHECKING: - from typing import Type from typing_extensions import Literal diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 6be4725d68d..d5ea2a99ee9 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -13,6 +13,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -30,8 +31,6 @@ from _pytest.warning_types import PytestUnknownMarkWarning if TYPE_CHECKING: - from typing import Type - from ..nodes import Node @@ -417,9 +416,7 @@ def __call__( # noqa: F811 *conditions: Union[str, bool], reason: str = ..., run: bool = ..., - raises: Union[ - "Type[BaseException]", Tuple["Type[BaseException]", ...] - ] = ..., + raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ..., strict: bool = ... ) -> MarkDecorator: ... diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index f8dbf826373..c8375e7cdb8 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -10,6 +10,7 @@ from typing import Optional from typing import Set from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -36,8 +37,6 @@ from _pytest.store import Store if TYPE_CHECKING: - from typing import Type - # Imported here due to circular import. from _pytest.main import Session from _pytest.warning_types import PytestWarning @@ -350,7 +349,7 @@ def addfinalizer(self, fin: Callable[[], object]) -> None: """ self.session._setupstate.addfinalizer(fin, self) - def getparent(self, cls: "Type[_NodeType]") -> Optional[_NodeType]: + def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]: """Get the next parent node (including self) which is an instance of the given class.""" current = self # type: Optional[Node] diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index a2ddc3a1f1a..cc70e72d4b1 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -5,13 +5,13 @@ from typing import Callable from typing import cast from typing import Optional +from typing import Type from typing import TypeVar TYPE_CHECKING = False # Avoid circular import through compat. if TYPE_CHECKING: from typing import NoReturn - from typing import Type # noqa: F401 (used in type string) from typing_extensions import Protocol else: # typing.Protocol is only available starting from Python 3.8. It is also @@ -84,7 +84,7 @@ def __init__( # Ideally would just be `exit.Exception = Exit` etc. _F = TypeVar("_F", bound=Callable[..., object]) -_ET = TypeVar("_ET", bound="Type[BaseException]") +_ET = TypeVar("_ET", bound=Type[BaseException]) class _WithException(Protocol[_F, _ET]): diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 7c7ac9348d6..29690f0f011 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -18,6 +18,7 @@ from typing import Optional from typing import Sequence from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import Union from weakref import WeakKeyDictionary @@ -49,7 +50,6 @@ from _pytest.tmpdir import TempdirFactory if TYPE_CHECKING: - from typing import Type from typing_extensions import Literal import pexpect @@ -420,7 +420,7 @@ def linecomp() -> "LineComp": @pytest.fixture(name="LineMatcher") -def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]": +def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: """A reference to the :class: `LineMatcher`. This is instantiable with a list of lines (without their trailing newlines). diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ee4d7d3f8a7..e0a295a065b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -22,6 +22,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import Union @@ -72,7 +73,6 @@ from _pytest.warning_types import PytestUnhandledCoroutineWarning if TYPE_CHECKING: - from typing import Type from typing_extensions import Literal from _pytest.fixtures import _Scope diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index e5489ec8328..b1f09030ade 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -13,7 +13,7 @@ from typing import Optional from typing import Pattern from typing import Tuple -from typing import TYPE_CHECKING +from typing import Type from typing import TypeVar from typing import Union @@ -23,9 +23,6 @@ from _pytest.compat import STRING_TYPES from _pytest.outcomes import fail -if TYPE_CHECKING: - from typing import Type - def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: at_str = " at {}".format(at) if at else "" @@ -560,7 +557,7 @@ def _is_numpy_array(obj: object) -> bool: @overload def raises( - expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], + expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], *, match: Optional[Union[str, Pattern[str]]] = ... ) -> "RaisesContext[_E]": @@ -569,7 +566,7 @@ def raises( @overload # noqa: F811 def raises( # noqa: F811 - expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], + expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], func: Callable[..., Any], *args: Any, **kwargs: Any @@ -578,9 +575,7 @@ def raises( # noqa: F811 def raises( # noqa: F811 - expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], - *args: Any, - **kwargs: Any + expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], *args: Any, **kwargs: Any ) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]: r"""Assert that a code block/function call raises ``expected_exception`` or raise a failure exception otherwise. @@ -738,7 +733,7 @@ def raises( # noqa: F811 class RaisesContext(Generic[_E]): def __init__( self, - expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], + expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], message: str, match_expr: Optional[Union[str, Pattern[str]]] = None, ) -> None: @@ -753,7 +748,7 @@ def __enter__(self) -> _pytest._code.ExceptionInfo[_E]: def __exit__( self, - exc_type: Optional["Type[BaseException]"], + exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> bool: @@ -764,9 +759,7 @@ def __exit__( if not issubclass(exc_type, self.expected_exception): return False # Cast to narrow the exception type now that it's verified. - exc_info = cast( - Tuple["Type[_E]", _E, TracebackType], (exc_type, exc_val, exc_tb) - ) + exc_info = cast(Tuple[Type[_E], _E, TracebackType], (exc_type, exc_val, exc_tb)) self.excinfo.fill_unfilled(exc_info) if self.match_expr is not None: self.excinfo.match(self.match_expr) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index a2e6b274197..a455e9e57f0 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -10,7 +10,7 @@ from typing import Optional from typing import Pattern from typing import Tuple -from typing import TYPE_CHECKING +from typing import Type from typing import TypeVar from typing import Union @@ -19,9 +19,6 @@ from _pytest.fixtures import fixture from _pytest.outcomes import fail -if TYPE_CHECKING: - from typing import Type - T = TypeVar("T") @@ -86,7 +83,7 @@ def deprecated_call( # noqa: F811 @overload def warns( - expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]], + expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], *, match: Optional[Union[str, Pattern[str]]] = ... ) -> "WarningsChecker": @@ -95,7 +92,7 @@ def warns( @overload # noqa: F811 def warns( # noqa: F811 - expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]], + expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], func: Callable[..., T], *args: Any, **kwargs: Any @@ -104,7 +101,7 @@ def warns( # noqa: F811 def warns( # noqa: F811 - expected_warning: Optional[Union["Type[Warning]", Tuple["Type[Warning]", ...]]], + expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], *args: Any, match: Optional[Union[str, Pattern[str]]] = None, **kwargs: Any @@ -187,7 +184,7 @@ def __len__(self) -> int: """The number of recorded warnings.""" return len(self._list) - def pop(self, cls: "Type[Warning]" = Warning) -> "warnings.WarningMessage": + def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage": """Pop the first recorded warning, raise exception if not exists.""" for i, w in enumerate(self._list): if issubclass(w.category, cls): @@ -214,7 +211,7 @@ def __enter__(self) -> "WarningsRecorder": # type: ignore def __exit__( self, - exc_type: Optional["Type[BaseException]"], + exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> None: @@ -234,7 +231,7 @@ class WarningsChecker(WarningsRecorder): def __init__( self, expected_warning: Optional[ - Union["Type[Warning]", Tuple["Type[Warning]", ...]] + Union[Type[Warning], Tuple[Type[Warning], ...]] ] = None, match_expr: Optional[Union[str, Pattern[str]]] = None, ) -> None: @@ -258,7 +255,7 @@ def __init__( def __exit__( self, - exc_type: Optional["Type[BaseException]"], + exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> None: diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 94c78581254..a6d593ccd27 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -8,6 +8,7 @@ from typing import List from typing import Optional from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -36,7 +37,6 @@ if TYPE_CHECKING: from typing import NoReturn - from typing_extensions import Type from typing_extensions import Literal from _pytest.runner import CallInfo @@ -199,7 +199,7 @@ def _to_json(self) -> Dict[str, Any]: return _report_to_json(self) @classmethod - def _from_json(cls: "Type[_R]", reportdict: Dict[str, object]) -> _R: + def _from_json(cls: Type[_R], reportdict: Dict[str, object]) -> _R: """Create either a TestReport or CollectReport, depending on the calling class. It is the callers responsibility to know which class to pass here. @@ -213,7 +213,7 @@ def _from_json(cls: "Type[_R]", reportdict: Dict[str, object]) -> _R: def _report_unserialization_failure( - type_name: str, report_class: "Type[BaseReport]", reportdict + type_name: str, report_class: Type[BaseReport], reportdict ) -> "NoReturn": url = "https://github.com/pytest-dev/pytest/issues" stream = StringIO() diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 74692e21145..e16fb4ab477 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -9,6 +9,7 @@ from typing import List from typing import Optional from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -33,7 +34,6 @@ from _pytest.outcomes import TEST_OUTCOME if TYPE_CHECKING: - from typing import Type from typing_extensions import Literal from _pytest.main import Session @@ -301,7 +301,9 @@ def from_call( cls, func: "Callable[[], TResult]", when: "Literal['collect', 'setup', 'call', 'teardown']", - reraise: "Optional[Union[Type[BaseException], Tuple[Type[BaseException], ...]]]" = None, + reraise: Optional[ + Union[Type[BaseException], Tuple[Type[BaseException], ...]] + ] = None, ) -> "CallInfo[TResult]": excinfo = None start = timing.time() diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 9be49bbfad4..cc505fdd7c2 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -6,7 +6,7 @@ from typing import Generator from typing import Optional from typing import Tuple -from typing import TYPE_CHECKING +from typing import Type import attr @@ -22,9 +22,6 @@ from _pytest.runner import CallInfo from _pytest.store import StoreKey -if TYPE_CHECKING: - from typing import Type - def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") @@ -194,7 +191,7 @@ class Xfail: reason = attr.ib(type=str) run = attr.ib(type=bool) strict = attr.ib(type=bool) - raises = attr.ib(type=Optional[Tuple["Type[BaseException]", ...]]) + raises = attr.ib(type=Optional[Tuple[Type[BaseException], ...]]) def evaluate_xfail_marks(item: Item) -> Optional[Xfail]: diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 6b466da96bf..62c6e90b71b 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -9,6 +9,7 @@ from typing import List from typing import Optional from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import Union @@ -33,7 +34,6 @@ if TYPE_CHECKING: import unittest - from typing import Type from _pytest.fixtures import _Scope diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index a7490dc776a..bd3a1d0b720 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -1,15 +1,12 @@ from typing import Any from typing import Generic -from typing import TYPE_CHECKING +from typing import Type from typing import TypeVar import attr from _pytest.compat import final -if TYPE_CHECKING: - from typing import Type # noqa: F401 (used in type string) - class PytestWarning(UserWarning): """Base class for all warnings emitted by pytest.""" @@ -105,7 +102,7 @@ class UnformattedWarning(Generic[_W]): as opposed to a direct message. """ - category = attr.ib(type="Type[_W]") + category = attr.ib(type=Type["_W"]) template = attr.ib(type=str) def format(self, **kwargs: Any) -> _W: diff --git a/testing/test_config.py b/testing/test_config.py index 02db737d6b9..02696fff265 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -6,7 +6,7 @@ from typing import List from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING +from typing import Type import attr import py.path @@ -29,9 +29,6 @@ from _pytest.pathlib import Path from _pytest.pytester import Testdir -if TYPE_CHECKING: - from typing import Type - class TestParseIni: @pytest.mark.parametrize( @@ -1936,7 +1933,7 @@ def test_strtobool(): ], ) def test_parse_warning_filter( - arg: str, escape: bool, expected: "Tuple[str, str, Type[Warning], str, int]" + arg: str, escape: bool, expected: Tuple[str, str, Type[Warning], str, int] ) -> None: assert parse_warning_filter(arg, escape=escape) == expected diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 52636f85123..f149e069561 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -4,7 +4,7 @@ import textwrap from typing import Dict from typing import Generator -from typing import TYPE_CHECKING +from typing import Type import py @@ -12,9 +12,6 @@ from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Testdir -if TYPE_CHECKING: - from typing import Type - @pytest.fixture def mp() -> Generator[MonkeyPatch, None, None]: @@ -354,7 +351,7 @@ class SampleInherit(Sample): @pytest.mark.parametrize( "Sample", [Sample, SampleInherit], ids=["new", "new-inherit"], ) -def test_issue156_undo_staticmethod(Sample: "Type[Sample]") -> None: +def test_issue156_undo_staticmethod(Sample: Type[Sample]) -> None: monkeypatch = MonkeyPatch() monkeypatch.setattr(Sample, "hello", None) diff --git a/testing/test_runner.py b/testing/test_runner.py index dff555534b7..d3b7729f728 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -5,7 +5,7 @@ from typing import Dict from typing import List from typing import Tuple -from typing import TYPE_CHECKING +from typing import Type import py @@ -17,9 +17,6 @@ from _pytest.config import ExitCode from _pytest.outcomes import OutcomeException -if TYPE_CHECKING: - from typing import Type - class TestSetupState: def test_setup(self, testdir) -> None: @@ -457,7 +454,7 @@ class TestClass(object): @pytest.mark.parametrize( "reporttype", reporttypes, ids=[x.__name__ for x in reporttypes] ) -def test_report_extra_parameters(reporttype: "Type[reports.BaseReport]") -> None: +def test_report_extra_parameters(reporttype: Type[reports.BaseReport]) -> None: args = list(inspect.signature(reporttype.__init__).parameters.keys())[1:] basekw = dict.fromkeys(args, []) # type: Dict[str, List[object]] report = reporttype(newthing=1, **basekw) From 53b5f64b4be8ba51dc5c9168f071c67da19bed40 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 19:57:16 -0700 Subject: [PATCH 667/823] py36+: resolve py36 TODOs --- src/_pytest/mark/structures.py | 20 +++++++++----------- testing/test_assertion.py | 5 ++--- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 6be4725d68d..14c7c177670 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -8,6 +8,7 @@ from typing import Iterator from typing import List from typing import Mapping +from typing import MutableMapping from typing import NamedTuple from typing import Optional from typing import Sequence @@ -94,8 +95,7 @@ def param( if isinstance(marks, MarkDecorator): marks = (marks,) else: - # TODO(py36): Change to collections.abc.Collection. - assert isinstance(marks, (collections.abc.Sequence, set)) + assert isinstance(marks, collections.abc.Collection) if id is not None: if not isinstance(id, str): @@ -475,13 +475,12 @@ def test_function(): # See TYPE_CHECKING above. if TYPE_CHECKING: - # TODO(py36): Change to builtin annotation syntax. - skip = _SkipMarkDecorator(Mark("skip", (), {})) - skipif = _SkipifMarkDecorator(Mark("skipif", (), {})) - xfail = _XfailMarkDecorator(Mark("xfail", (), {})) - parametrize = _ParametrizeMarkDecorator(Mark("parametrize", (), {})) - usefixtures = _UsefixturesMarkDecorator(Mark("usefixtures", (), {})) - filterwarnings = _FilterwarningsMarkDecorator(Mark("filterwarnings", (), {})) + skip: _SkipMarkDecorator + skipif: _SkipifMarkDecorator + xfail: _XfailMarkDecorator + parametrize: _ParametrizeMarkDecorator + usefixtures: _UsefixturesMarkDecorator + filterwarnings: _FilterwarningsMarkDecorator def __getattr__(self, name: str) -> MarkDecorator: if name[0] == "_": @@ -527,9 +526,8 @@ def __getattr__(self, name: str) -> MarkDecorator: MARK_GEN = MarkGenerator() -# TODO(py36): inherit from typing.MutableMapping[str, Any]. @final -class NodeKeywords(collections.abc.MutableMapping): # type: ignore[type-arg] +class NodeKeywords(MutableMapping[str, Any]): def __init__(self, node: "Node") -> None: self.node = node self.parent = node.parent diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 258be48b82f..d6dc9fc986e 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1,8 +1,8 @@ -import collections.abc import sys import textwrap from typing import Any from typing import List +from typing import MutableSequence from typing import Optional import attr @@ -637,8 +637,7 @@ def test_frozenzet(self) -> None: def test_Sequence(self) -> None: # Test comparing with a Sequence subclass. - # TODO(py36): Inherit from typing.MutableSequence[int]. - class TestSequence(collections.abc.MutableSequence): # type: ignore[type-arg] + class TestSequence(MutableSequence[int]): def __init__(self, iterable): self.elements = list(iterable) From 7705e5e624e269e604096d4c8dd7aad3e933c8f3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 3 Oct 2020 13:10:03 +0300 Subject: [PATCH 668/823] doc: patch Sphinx to detect our `@final` for marking classes as `final` Thanks to Dominic Davis-Foster for code & assistance. --- changelog/7780.doc.rst | 1 + doc/en/conf.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 changelog/7780.doc.rst diff --git a/changelog/7780.doc.rst b/changelog/7780.doc.rst new file mode 100644 index 00000000000..631873b156e --- /dev/null +++ b/changelog/7780.doc.rst @@ -0,0 +1 @@ +Classes which should not be inherited from are now marked ``final class`` in the API reference. diff --git a/doc/en/conf.py b/doc/en/conf.py index 7e96c5de9fa..2f3a2baf44b 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -15,8 +15,10 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. +import ast import os import sys +from typing import List from typing import TYPE_CHECKING from _pytest import __version__ as version @@ -398,3 +400,22 @@ 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 From 7f0d2beb508068145a7e929fc760726e1a450915 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 13:07:37 -0700 Subject: [PATCH 669/823] py36+: remove _pytest.compat.overload --- src/_pytest/_code/code.py | 10 ++++------ src/_pytest/_code/source.py | 9 ++++----- src/_pytest/assertion/rewrite.py | 2 +- src/_pytest/compat.py | 18 +++++------------- src/_pytest/fixtures.py | 10 +++++----- src/_pytest/main.py | 8 ++++---- src/_pytest/mark/structures.py | 18 ++++++++---------- src/_pytest/monkeypatch.py | 8 ++++---- src/_pytest/nodes.py | 8 ++++---- src/_pytest/pytester.py | 22 +++++++++++----------- src/_pytest/python_api.py | 8 ++++---- src/_pytest/recwarn.py | 16 +++++++--------- 12 files changed, 61 insertions(+), 76 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index ce63b4b16bf..0f13e699bbf 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -17,6 +17,7 @@ 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 Set @@ -41,7 +42,6 @@ from _pytest._io.saferepr import saferepr from _pytest.compat import final from _pytest.compat import get_real_func -from _pytest.compat import overload from _pytest.pathlib import Path if TYPE_CHECKING: @@ -346,13 +346,11 @@ def cut( def __getitem__(self, key: int) -> TracebackEntry: ... - @overload # noqa: F811 - def __getitem__(self, key: slice) -> "Traceback": # noqa: F811 + @overload + def __getitem__(self, key: slice) -> "Traceback": ... - def __getitem__( # noqa: F811 - self, key: Union[int, slice] - ) -> Union[TracebackEntry, "Traceback"]: + def __getitem__(self, key: Union[int, slice]) -> Union[TracebackEntry, "Traceback"]: if isinstance(key, slice): return self.__class__(super().__getitem__(key)) else: diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 4ba18aa6361..028ff48a3fe 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -8,11 +8,10 @@ from typing import Iterator from typing import List from typing import Optional +from typing import overload from typing import Tuple from typing import Union -from _pytest.compat import overload - class Source: """An immutable object holding a source code fragment. @@ -46,11 +45,11 @@ def __eq__(self, other: object) -> bool: def __getitem__(self, key: int) -> str: ... - @overload # noqa: F811 - def __getitem__(self, key: slice) -> "Source": # noqa: F811 + @overload + def __getitem__(self, key: slice) -> "Source": ... - def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: F811 + def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: if isinstance(key, int): return self.lines[key] else: diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 28a206c27a3..f50ef232a17 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -41,7 +41,7 @@ from _pytest.store import StoreKey if TYPE_CHECKING: - from _pytest.assertion import AssertionState # noqa: F401 + from _pytest.assertion import AssertionState assertstate_key = StoreKey["AssertionState"]() diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 8fa58bccf16..ef4164268e2 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -11,7 +11,6 @@ from typing import Callable from typing import Generic from typing import Optional -from typing import overload as overload from typing import Tuple from typing import TYPE_CHECKING from typing import TypeVar @@ -326,12 +325,6 @@ def safe_isclass(obj: object) -> bool: return False -if sys.version_info < (3, 5, 2): - - def overload(f): # noqa: F811 - return f - - if TYPE_CHECKING: if sys.version_info >= (3, 8): from typing import final as final @@ -341,13 +334,14 @@ def overload(f): # noqa: F811 from typing import final as final else: - def final(f): # noqa: F811 + def final(f): return f if sys.version_info >= (3, 8): from functools import cached_property as cached_property else: + from typing import overload from typing import Type class cached_property(Generic[_S, _T]): @@ -363,13 +357,11 @@ def __get__( ) -> "cached_property[_S, _T]": ... - @overload # noqa: F811 - def __get__( # noqa: F811 - self, instance: _S, owner: Optional[Type[_S]] = ... - ) -> _T: + @overload + def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T: ... - def __get__(self, instance, owner=None): # noqa: F811 + def __get__(self, instance, owner=None): if instance is None: return self value = instance.__dict__[self.func.__name__] = self.func(instance) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d793ea37f1e..84e748941b2 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -16,6 +16,7 @@ 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 @@ -43,7 +44,6 @@ from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import order_preserving_dict -from _pytest.compat import overload from _pytest.compat import safe_getattr from _pytest.config import _PluggyPlugin from _pytest.config import Config @@ -462,7 +462,7 @@ def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]": @property def config(self) -> Config: """The pytest config object associated with this request.""" - return self._pyfuncitem.config # type: ignore[no-any-return] # noqa: F723 + return self._pyfuncitem.config # type: ignore[no-any-return] @property def function(self): @@ -1225,8 +1225,8 @@ def fixture( ... -@overload # noqa: F811 -def fixture( # noqa: F811 +@overload +def fixture( fixture_function: None = ..., *, scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ..., @@ -1243,7 +1243,7 @@ def fixture( # noqa: F811 ... -def fixture( # noqa: F811 +def fixture( fixture_function: Optional[_FixtureFunction] = None, *, scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function", diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 31dc28e5d0e..624f8abb2a4 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -11,6 +11,7 @@ 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 @@ -24,7 +25,6 @@ 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 @@ -562,13 +562,13 @@ def perform_collect( ) -> Sequence[nodes.Item]: ... - @overload # noqa: F811 - def perform_collect( # noqa: F811 + @overload + def perform_collect( self, args: Optional[Sequence[str]] = ..., genitems: bool = ... ) -> Sequence[Union[nodes.Item, nodes.Collector]]: ... - def perform_collect( # noqa: F811 + def perform_collect( self, args: Optional[Sequence[str]] = None, genitems: bool = True ) -> Sequence[Union[nodes.Item, nodes.Collector]]: """Perform the collection phase for this session. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 23c552d9897..8542434a52b 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -11,6 +11,7 @@ from typing import MutableMapping from typing import NamedTuple from typing import Optional +from typing import overload from typing import Sequence from typing import Set from typing import Tuple @@ -26,7 +27,6 @@ from ..compat import final from ..compat import NOTSET from ..compat import NotSetType -from ..compat import overload from _pytest.config import Config from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning @@ -330,13 +330,11 @@ def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc] pass - @overload # noqa: F811 - def __call__( # noqa: F811 - self, *args: object, **kwargs: object - ) -> "MarkDecorator": + @overload + def __call__(self, *args: object, **kwargs: object) -> "MarkDecorator": pass - def __call__(self, *args: object, **kwargs: object): # noqa: F811 + def __call__(self, *args: object, **kwargs: object): """Call the MarkDecorator.""" if args and not kwargs: func = args[0] @@ -391,8 +389,8 @@ class _SkipMarkDecorator(MarkDecorator): def __call__(self, arg: _Markable) -> _Markable: ... - @overload # noqa: F811 - def __call__(self, reason: str = ...) -> "MarkDecorator": # noqa: F811 + @overload + def __call__(self, reason: str = ...) -> "MarkDecorator": ... class _SkipifMarkDecorator(MarkDecorator): @@ -409,8 +407,8 @@ class _XfailMarkDecorator(MarkDecorator): def __call__(self, arg: _Markable) -> _Markable: ... - @overload # noqa: F811 - def __call__( # noqa: F811 + @overload + def __call__( self, condition: Union[str, bool] = ..., *conditions: Union[str, bool], diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index bbd96779da5..8251fd9bd1c 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -9,13 +9,13 @@ from typing import List from typing import MutableMapping from typing import Optional +from typing import overload from typing import Tuple from typing import TypeVar from typing import Union import pytest from _pytest.compat import final -from _pytest.compat import overload from _pytest.fixtures import fixture from _pytest.pathlib import Path @@ -156,13 +156,13 @@ def setattr( ) -> None: ... - @overload # noqa: F811 - def setattr( # noqa: F811 + @overload + def setattr( self, target: object, name: str, value: object, raising: bool = ..., ) -> None: ... - def setattr( # noqa: F811 + def setattr( self, target: Union[str, object], name: Union[object, str], diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index c8375e7cdb8..f3568b1677e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -8,6 +8,7 @@ from typing import Iterator from typing import List from typing import Optional +from typing import overload from typing import Set from typing import Tuple from typing import Type @@ -22,7 +23,6 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest.compat import cached_property -from _pytest.compat import overload from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH @@ -316,11 +316,11 @@ def iter_markers_with_node( def get_closest_marker(self, name: str) -> Optional[Mark]: ... - @overload # noqa: F811 - def get_closest_marker(self, name: str, default: Mark) -> Mark: # noqa: F811 + @overload + def get_closest_marker(self, name: str, default: Mark) -> Mark: ... - def get_closest_marker( # noqa: F811 + def get_closest_marker( self, name: str, default: Optional[Mark] = None ) -> Optional[Mark]: """Return the first marker matching the name, from closest (for diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 29690f0f011..df549ea1d6e 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -16,6 +16,7 @@ from typing import Iterable from typing import List from typing import Optional +from typing import overload from typing import Sequence from typing import Tuple from typing import Type @@ -31,7 +32,6 @@ from _pytest._code import Source from _pytest.capture import _get_multicapture from _pytest.compat import final -from _pytest.compat import overload from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode @@ -277,14 +277,14 @@ def getreports( ) -> Sequence[CollectReport]: ... - @overload # noqa: F811 - def getreports( # noqa: F811 + @overload + def getreports( self, names: "Literal['pytest_runtest_logreport']", ) -> Sequence[TestReport]: ... - @overload # noqa: F811 - def getreports( # noqa: F811 + @overload + def getreports( self, names: Union[str, Iterable[str]] = ( "pytest_collectreport", @@ -293,7 +293,7 @@ def getreports( # noqa: F811 ) -> Sequence[Union[CollectReport, TestReport]]: ... - def getreports( # noqa: F811 + def getreports( self, names: Union[str, Iterable[str]] = ( "pytest_collectreport", @@ -340,14 +340,14 @@ def getfailures( ) -> Sequence[CollectReport]: ... - @overload # noqa: F811 - def getfailures( # noqa: F811 + @overload + def getfailures( self, names: "Literal['pytest_runtest_logreport']", ) -> Sequence[TestReport]: ... - @overload # noqa: F811 - def getfailures( # noqa: F811 + @overload + def getfailures( self, names: Union[str, Iterable[str]] = ( "pytest_collectreport", @@ -356,7 +356,7 @@ def getfailures( # noqa: F811 ) -> Sequence[Union[CollectReport, TestReport]]: ... - def getfailures( # noqa: F811 + def getfailures( self, names: Union[str, Iterable[str]] = ( "pytest_collectreport", diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index b1f09030ade..3c1993be579 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -11,6 +11,7 @@ from typing import cast from typing import Generic from typing import Optional +from typing import overload from typing import Pattern from typing import Tuple from typing import Type @@ -19,7 +20,6 @@ import _pytest._code from _pytest.compat import final -from _pytest.compat import overload from _pytest.compat import STRING_TYPES from _pytest.outcomes import fail @@ -564,8 +564,8 @@ def raises( ... -@overload # noqa: F811 -def raises( # noqa: F811 +@overload +def raises( expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], func: Callable[..., Any], *args: Any, @@ -574,7 +574,7 @@ def raises( # noqa: F811 ... -def raises( # noqa: F811 +def raises( expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], *args: Any, **kwargs: Any ) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]: r"""Assert that a code block/function call raises ``expected_exception`` diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index a455e9e57f0..cc499138c77 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -8,6 +8,7 @@ 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 @@ -15,7 +16,6 @@ from typing import Union from _pytest.compat import final -from _pytest.compat import overload from _pytest.fixtures import fixture from _pytest.outcomes import fail @@ -43,14 +43,12 @@ def deprecated_call( ... -@overload # noqa: F811 -def deprecated_call( # noqa: F811 - func: Callable[..., T], *args: Any, **kwargs: Any -) -> T: +@overload +def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: ... -def deprecated_call( # noqa: F811 +def deprecated_call( func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any ) -> Union["WarningsRecorder", Any]: """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning``. @@ -90,8 +88,8 @@ def warns( ... -@overload # noqa: F811 -def warns( # noqa: F811 +@overload +def warns( expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], func: Callable[..., T], *args: Any, @@ -100,7 +98,7 @@ def warns( # noqa: F811 ... -def warns( # noqa: F811 +def warns( expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], *args: Any, match: Optional[Union[str, Pattern[str]]] = None, From f295b0267d14017c95144ed191fbc8ba75aeb789 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 13:09:35 -0700 Subject: [PATCH 670/823] py36+: update the target version of black to py36 --- pyproject.toml | 2 +- src/_pytest/_io/saferepr.py | 2 +- src/_pytest/_io/terminalwriter.py | 2 +- src/_pytest/compat.py | 2 +- src/_pytest/config/__init__.py | 2 +- src/_pytest/doctest.py | 2 +- src/_pytest/fixtures.py | 8 ++++---- src/_pytest/mark/__init__.py | 2 +- src/_pytest/mark/structures.py | 10 +++++----- src/_pytest/pathlib.py | 2 +- src/_pytest/pytester.py | 4 ++-- src/_pytest/python.py | 2 +- src/_pytest/python_api.py | 4 ++-- src/_pytest/recwarn.py | 6 +++--- src/_pytest/reports.py | 4 ++-- src/_pytest/terminal.py | 2 +- 16 files changed, 28 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aee467fcf12..443b94c26a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,4 +102,4 @@ template = "changelog/_template.rst" showcontent = true [tool.black] -target-version = ['py35'] +target-version = ['py36'] diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 9a4975f61ad..5eb1e088905 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -122,7 +122,7 @@ def _pformat_dispatch( width: int = 80, depth: Optional[int] = None, *, - compact: bool = False + compact: bool = False, ) -> str: return AlwaysDispatchingPrettyPrinter( indent=indent, width=width, depth=depth, compact=compact diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index a9404ebcc16..3dc25c6fef1 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -109,7 +109,7 @@ def sep( sepchar: str, title: Optional[str] = None, fullwidth: Optional[int] = None, - **markup: bool + **markup: bool, ) -> None: if fullwidth is None: fullwidth = self.fullwidth diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 8fa58bccf16..f96620c1806 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -116,7 +116,7 @@ def getfuncargnames( *, name: str = "", is_method: bool = False, - cls: Optional[type] = None + cls: Optional[type] = None, ) -> Tuple[str, ...]: """Return the names of a function's mandatory arguments. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 799cc19c613..c939de73b2e 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -866,7 +866,7 @@ def __init__( self, pluginmanager: PytestPluginManager, *, - invocation_params: Optional[InvocationParams] = None + invocation_params: Optional[InvocationParams] = None, ) -> None: from .argparsing import Parser, FILE_OR_DIR diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 817c9f9a841..bdbbc51976a 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -260,7 +260,7 @@ def from_parent( # type: ignore *, name: str, runner: "doctest.DocTestRunner", - dtest: "doctest.DocTest" + dtest: "doctest.DocTest", ): # incompatible signature due to to imposed limits on sublcass """The public named constructor.""" diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d793ea37f1e..ce61b57f972 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1220,7 +1220,7 @@ def fixture( Callable[[Any], Optional[object]], ] ] = ..., - name: Optional[str] = ... + name: Optional[str] = ..., ) -> _FixtureFunction: ... @@ -1238,7 +1238,7 @@ def fixture( # noqa: F811 Callable[[Any], Optional[object]], ] ] = ..., - name: Optional[str] = None + name: Optional[str] = None, ) -> FixtureFunctionMarker: ... @@ -1255,7 +1255,7 @@ def fixture( # noqa: F811 Callable[[Any], Optional[object]], ] ] = None, - name: Optional[str] = None + name: Optional[str] = None, ) -> Union[FixtureFunctionMarker, _FixtureFunction]: """Decorator to mark a fixture factory function. @@ -1325,7 +1325,7 @@ def yield_fixture( params=None, autouse=False, ids=None, - name=None + name=None, ): """(Return a) decorator to mark a yield-fixture factory function. diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index f6907b8fd3d..cc6e80e8015 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -47,7 +47,7 @@ def param( *values: object, marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (), - id: Optional[str] = None + id: Optional[str] = None, ) -> ParameterSet: """Specify a parameter in `pytest.mark.parametrize`_ calls or :ref:`parametrized fixtures `. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 23c552d9897..c5a3b8d9997 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -89,7 +89,7 @@ def param( cls, *values: object, marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (), - id: Optional[str] = None + id: Optional[str] = None, ) -> "ParameterSet": if isinstance(marks, MarkDecorator): marks = (marks,) @@ -138,7 +138,7 @@ def _parse_parametrize_args( argnames: Union[str, List[str], Tuple[str, ...]], argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], *args, - **kwargs + **kwargs, ) -> Tuple[Union[List[str], Tuple[str, ...]], bool]: if not isinstance(argnames, (tuple, list)): argnames = [x.strip() for x in argnames.split(",") if x.strip()] @@ -400,7 +400,7 @@ def __call__( # type: ignore[override] self, condition: Union[str, bool] = ..., *conditions: Union[str, bool], - reason: str = ... + reason: str = ..., ) -> MarkDecorator: ... @@ -417,7 +417,7 @@ def __call__( # noqa: F811 reason: str = ..., run: bool = ..., raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ..., - strict: bool = ... + strict: bool = ..., ) -> MarkDecorator: ... @@ -434,7 +434,7 @@ def __call__( # type: ignore[override] Callable[[Any], Optional[object]], ] ] = ..., - scope: Optional[_Scope] = ... + scope: Optional[_Scope] = ..., ) -> MarkDecorator: ... diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index de8917be27a..06aba339145 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -441,7 +441,7 @@ class ImportPathMismatchError(ImportError): def import_path( p: Union[str, py.path.local, Path], *, - mode: Union[str, ImportMode] = ImportMode.prepend + mode: Union[str, ImportMode] = ImportMode.prepend, ) -> ModuleType: """Import and return a module from the given path, which can be a file (a module) or a directory (a package). diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 29690f0f011..6ced7e25087 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1211,7 +1211,7 @@ def popen( stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=CLOSE_STDIN, - **kw + **kw, ): """Invoke subprocess.Popen. @@ -1530,7 +1530,7 @@ def _match_lines( match_func: Callable[[str, str], bool], match_nickname: str, *, - consecutive: bool = False + consecutive: bool = False, ) -> None: """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e0a295a065b..1acf2c0e5a3 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -975,7 +975,7 @@ def parametrize( ] = None, scope: "Optional[_Scope]" = None, *, - _param_mark: Optional[Mark] = None + _param_mark: Optional[Mark] = None, ) -> None: """Add new invocations to the underlying test function using the list of argvalues for the given argnames. Parametrization is performed diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index b1f09030ade..130e816ee1a 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -559,7 +559,7 @@ def _is_numpy_array(obj: object) -> bool: def raises( expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], *, - match: Optional[Union[str, Pattern[str]]] = ... + match: Optional[Union[str, Pattern[str]]] = ..., ) -> "RaisesContext[_E]": ... @@ -569,7 +569,7 @@ def raises( # noqa: F811 expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], func: Callable[..., Any], *args: Any, - **kwargs: Any + **kwargs: Any, ) -> _pytest._code.ExceptionInfo[_E]: ... diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index a455e9e57f0..05969218306 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -85,7 +85,7 @@ def deprecated_call( # noqa: F811 def warns( expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], *, - match: Optional[Union[str, Pattern[str]]] = ... + match: Optional[Union[str, Pattern[str]]] = ..., ) -> "WarningsChecker": ... @@ -95,7 +95,7 @@ def warns( # noqa: F811 expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], func: Callable[..., T], *args: Any, - **kwargs: Any + **kwargs: Any, ) -> T: ... @@ -104,7 +104,7 @@ def warns( # noqa: F811 expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], *args: Any, match: Optional[Union[str, Pattern[str]]] = None, - **kwargs: Any + **kwargs: Any, ) -> Union["WarningsChecker", Any]: r"""Assert that code raises a particular class of warning. diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index a6d593ccd27..9bf5b02a80b 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -246,7 +246,7 @@ def __init__( sections: Iterable[Tuple[str, str]] = (), duration: float = 0, user_properties: Optional[Iterable[Tuple[str, object]]] = None, - **extra + **extra, ) -> None: #: Normalized collection nodeid. self.nodeid = nodeid @@ -348,7 +348,7 @@ def __init__( longrepr, result: Optional[List[Union[Item, Collector]]], sections: Iterable[Tuple[str, str]] = (), - **extra + **extra, ) -> None: #: Normalized collection nodeid. self.nodeid = nodeid diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 21634cbbcf7..881967a008e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -450,7 +450,7 @@ def write_sep( sep: str, title: Optional[str] = None, fullwidth: Optional[int] = None, - **markup: bool + **markup: bool, ) -> None: self.ensure_newline() self._tw.sep(sep, title, fullwidth, **markup) From fb1d550aac0bb092d0156b9ba46fe4082c907a10 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 3 Oct 2020 08:08:14 -0700 Subject: [PATCH 671/823] py36+: remove rexport of Path and PurePath --- src/_pytest/_code/code.py | 2 +- src/_pytest/assertion/rewrite.py | 4 ++-- src/_pytest/cacheprovider.py | 2 +- src/_pytest/compat.py | 3 +-- src/_pytest/config/__init__.py | 2 +- src/_pytest/config/findpaths.py | 2 +- src/_pytest/logging.py | 2 +- src/_pytest/main.py | 2 +- src/_pytest/monkeypatch.py | 2 +- src/_pytest/nodes.py | 2 +- src/_pytest/pathlib.py | 3 --- src/_pytest/pytester.py | 2 +- src/_pytest/reports.py | 2 +- src/_pytest/terminal.py | 2 +- src/_pytest/tmpdir.py | 2 +- testing/python/fixtures.py | 2 +- testing/test_assertrewrite.py | 2 +- testing/test_collection.py | 2 +- testing/test_config.py | 2 +- testing/test_conftest.py | 2 +- testing/test_findpaths.py | 2 +- testing/test_junitxml.py | 2 +- testing/test_main.py | 2 +- testing/test_pathlib.py | 2 +- testing/test_reports.py | 2 +- testing/test_terminal.py | 2 +- testing/test_tmpdir.py | 2 +- 27 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 0f13e699bbf..d6140b8dc11 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -5,6 +5,7 @@ from inspect import CO_VARARGS from inspect import CO_VARKEYWORDS from io import StringIO +from pathlib import Path from traceback import format_exception_only from types import CodeType from types import FrameType @@ -42,7 +43,6 @@ from _pytest._io.saferepr import saferepr from _pytest.compat import final from _pytest.compat import get_real_func -from _pytest.pathlib import Path if TYPE_CHECKING: from typing_extensions import Literal diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index f50ef232a17..1b504115ce7 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -13,6 +13,8 @@ import sys import tokenize import types +from pathlib import Path +from pathlib import PurePath from typing import Callable from typing import Dict from typing import IO @@ -36,8 +38,6 @@ from _pytest.config import Config from _pytest.main import Session from _pytest.pathlib import fnmatch_ex -from _pytest.pathlib import Path -from _pytest.pathlib import PurePath from _pytest.store import StoreKey if TYPE_CHECKING: diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index b04305ed9d2..5a1070b7758 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -3,6 +3,7 @@ # pytest-cache version. import json import os +from pathlib import Path from typing import Dict from typing import Generator from typing import Iterable @@ -15,7 +16,6 @@ import py import pytest -from .pathlib import Path from .pathlib import resolve_from_str from .pathlib import rm_rf from .reports import CollectReport diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 27dacb30360..a7e270859fe 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -7,6 +7,7 @@ from contextlib import contextmanager from inspect import Parameter from inspect import signature +from pathlib import Path from typing import Any from typing import Callable from typing import Generic @@ -76,8 +77,6 @@ def is_async_function(func: object) -> bool: def getlocation(function, curdir: Optional[str] = None) -> str: - from _pytest.pathlib import Path - function = get_real_func(function) fn = Path(inspect.getfile(function)) lineno = function.__code__.co_firstlineno diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c939de73b2e..9e7cdbd00e6 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -12,6 +12,7 @@ import types import warnings from functools import lru_cache +from pathlib import Path from types import TracebackType from typing import Any from typing import Callable @@ -52,7 +53,6 @@ from _pytest.pathlib import bestrelpath from _pytest.pathlib import import_path from _pytest.pathlib import ImportMode -from _pytest.pathlib import Path from _pytest.store import Store from _pytest.warning_types import PytestConfigWarning diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index ad2799777c4..8327e844924 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -1,4 +1,5 @@ import os +from pathlib import Path from typing import Dict from typing import Iterable from typing import List @@ -14,7 +15,6 @@ from _pytest.outcomes import fail from _pytest.pathlib import absolutepath from _pytest.pathlib import commonpath -from _pytest.pathlib import Path if TYPE_CHECKING: from . import Config diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index c277ba5320c..904e27ee4b3 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -5,6 +5,7 @@ import sys from contextlib import contextmanager from io import StringIO +from pathlib import Path from typing import AbstractSet from typing import Dict from typing import Generator @@ -27,7 +28,6 @@ from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureRequest from _pytest.main import Session -from _pytest.pathlib import Path from _pytest.store import StoreKey from _pytest.terminal import TerminalReporter diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 624f8abb2a4..bb11df4eab8 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -5,6 +5,7 @@ import importlib import os import sys +from pathlib import Path from typing import Callable from typing import Dict from typing import FrozenSet @@ -36,7 +37,6 @@ from _pytest.outcomes import exit from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath -from _pytest.pathlib import Path from _pytest.pathlib import visit from _pytest.reports import CollectReport from _pytest.reports import TestReport diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 8251fd9bd1c..bca12cd7a17 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -4,6 +4,7 @@ import sys import warnings from contextlib import contextmanager +from pathlib import Path from typing import Any from typing import Generator from typing import List @@ -17,7 +18,6 @@ import pytest from _pytest.compat import final from _pytest.fixtures import fixture -from _pytest.pathlib import Path RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index f3568b1677e..1489d097718 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,6 +1,7 @@ import os import warnings from functools import lru_cache +from pathlib import Path from typing import Any from typing import Callable from typing import Dict @@ -33,7 +34,6 @@ from _pytest.mark.structures import NodeKeywords from _pytest.outcomes import fail from _pytest.pathlib import absolutepath -from _pytest.pathlib import Path from _pytest.store import Store if TYPE_CHECKING: diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 06aba339145..75663ee6293 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -32,9 +32,6 @@ from _pytest.outcomes import skip from _pytest.warning_types import PytestWarning -__all__ = ["Path", "PurePath"] - - LOCK_TIMEOUT = 60 * 60 * 3 diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 2681f277955..834069a09b4 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -10,6 +10,7 @@ import traceback from fnmatch import fnmatch from io import StringIO +from pathlib import Path from typing import Callable from typing import Dict from typing import Generator @@ -43,7 +44,6 @@ from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.pathlib import make_numbered_dir -from _pytest.pathlib import Path from _pytest.python import Module from _pytest.reports import CollectReport from _pytest.reports import TestReport diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 9bf5b02a80b..c60137d3888 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,4 +1,5 @@ from io import StringIO +from pathlib import Path from pprint import pprint from typing import Any from typing import cast @@ -33,7 +34,6 @@ from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.outcomes import skip -from _pytest.pathlib import Path if TYPE_CHECKING: from typing import NoReturn diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 881967a008e..5b7a09c6435 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -9,6 +9,7 @@ import sys import warnings from functools import partial +from pathlib import Path from typing import Any from typing import Callable from typing import Dict @@ -43,7 +44,6 @@ from _pytest.nodes import Node from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath -from _pytest.pathlib import Path from _pytest.reports import BaseReport from _pytest.reports import CollectReport from _pytest.reports import TestReport diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 06bd764d450..02a613483a3 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -2,6 +2,7 @@ import os import re import tempfile +from pathlib import Path from typing import Optional import attr @@ -12,7 +13,6 @@ from .pathlib import LOCK_TIMEOUT from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup -from .pathlib import Path from _pytest.compat import final from _pytest.config import Config from _pytest.fixtures import FixtureRequest diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 9ae5a91db43..a85ebdf8e3d 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1,12 +1,12 @@ import sys import textwrap +from pathlib import Path import pytest from _pytest import fixtures from _pytest.compat import getfuncargnames from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest -from _pytest.pathlib import Path from _pytest.pytester import get_public_names diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index ad3089a23c0..98dea5d8854 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -9,6 +9,7 @@ import textwrap import zipfile from functools import partial +from pathlib import Path from typing import Dict from typing import List from typing import Mapping @@ -28,7 +29,6 @@ from _pytest.assertion.rewrite import rewrite_asserts from _pytest.config import ExitCode from _pytest.pathlib import make_numbered_dir -from _pytest.pathlib import Path from _pytest.pytester import Testdir diff --git a/testing/test_collection.py b/testing/test_collection.py index 3cb342a93d5..841aa358b96 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -2,12 +2,12 @@ import pprint import sys import textwrap +from pathlib import Path import pytest from _pytest.config import ExitCode from _pytest.main import _in_venv from _pytest.main import Session -from _pytest.pathlib import Path from _pytest.pathlib import symlink_or_skip from _pytest.pytester import Testdir diff --git a/testing/test_config.py b/testing/test_config.py index 02696fff265..7a0c135ef39 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -2,6 +2,7 @@ import re import sys import textwrap +from pathlib import Path from typing import Dict from typing import List from typing import Sequence @@ -26,7 +27,6 @@ from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import locate_config from _pytest.monkeypatch import MonkeyPatch -from _pytest.pathlib import Path from _pytest.pytester import Testdir diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 5a476408013..db56702041b 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,12 +1,12 @@ import os import textwrap +from pathlib import Path import py import pytest from _pytest.config import ExitCode from _pytest.config import PytestPluginManager -from _pytest.pathlib import Path from _pytest.pathlib import symlink_or_skip diff --git a/testing/test_findpaths.py b/testing/test_findpaths.py index 974dcf8f3cd..af6aeb3a56d 100644 --- a/testing/test_findpaths.py +++ b/testing/test_findpaths.py @@ -1,10 +1,10 @@ +from pathlib import Path from textwrap import dedent import pytest from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import get_dirs_from_args from _pytest.config.findpaths import load_config_dict_from_file -from _pytest.pathlib import Path class TestLoadConfigDictFromFile: diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 5a5610a6078..23ef3f34773 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,6 +1,7 @@ import os import platform from datetime import datetime +from pathlib import Path from typing import cast from typing import List from typing import Tuple @@ -14,7 +15,6 @@ from _pytest.config import Config from _pytest.junitxml import bin_xml_escape from _pytest.junitxml import LogXML -from _pytest.pathlib import Path from _pytest.reports import BaseReport from _pytest.reports import TestReport from _pytest.store import Store diff --git a/testing/test_main.py b/testing/test_main.py index 5b45ec6b5bd..8ec7b8111cd 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -1,6 +1,7 @@ import argparse import os import re +from pathlib import Path from typing import Optional import py.path @@ -10,7 +11,6 @@ from _pytest.config import UsageError from _pytest.main import resolve_collection_argument from _pytest.main import validate_basetemp -from _pytest.pathlib import Path from _pytest.pytester import Testdir diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 41228d6b095..e37b33847ee 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -1,6 +1,7 @@ import os.path import sys import unittest.mock +from pathlib import Path from textwrap import dedent import py @@ -15,7 +16,6 @@ from _pytest.pathlib import import_path from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import maybe_delete_a_numbered_dir -from _pytest.pathlib import Path from _pytest.pathlib import resolve_package_path diff --git a/testing/test_reports.py b/testing/test_reports.py index dbe94896207..67ace3943d6 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -1,4 +1,5 @@ import sys +from pathlib import Path from typing import Sequence from typing import Union @@ -6,7 +7,6 @@ from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionRepr from _pytest.config import Config -from _pytest.pathlib import Path from _pytest.pytester import Testdir from _pytest.reports import CollectReport from _pytest.reports import TestReport diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 9a284063137..1ff308fa603 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -4,6 +4,7 @@ import sys import textwrap from io import StringIO +from pathlib import Path from typing import cast from typing import Dict from typing import List @@ -19,7 +20,6 @@ from _pytest.config import Config from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch -from _pytest.pathlib import Path from _pytest.pytester import Testdir from _pytest.reports import BaseReport from _pytest.reports import CollectReport diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index cc03385f3a9..d4c21c98537 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -1,6 +1,7 @@ import os import stat import sys +from pathlib import Path from typing import Callable from typing import cast from typing import List @@ -15,7 +16,6 @@ from _pytest.pathlib import make_numbered_dir from _pytest.pathlib import maybe_delete_a_numbered_dir from _pytest.pathlib import on_rm_rf_error -from _pytest.pathlib import Path from _pytest.pathlib import register_cleanup_lock_removal from _pytest.pathlib import rm_rf from _pytest.tmpdir import get_user From fd74dd3dcbcb7505b619342548db957cc80a7676 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 3 Oct 2020 22:37:28 +0300 Subject: [PATCH 672/823] Merge pull request #7849 from pytest-dev/release-6.1.1 Prepare release 6.1.1 (cherry picked from commit 69d903260d39f50ef7233348e1521000710cc5ba) --- changelog/7807.bugfix.rst | 1 - changelog/7814.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-6.1.1.rst | 18 ++++++++++++++++++ doc/en/changelog.rst | 12 ++++++++++++ doc/en/getting-started.rst | 2 +- 6 files changed, 32 insertions(+), 3 deletions(-) delete mode 100644 changelog/7807.bugfix.rst delete mode 100644 changelog/7814.bugfix.rst create mode 100644 doc/en/announce/release-6.1.1.rst diff --git a/changelog/7807.bugfix.rst b/changelog/7807.bugfix.rst deleted file mode 100644 index 93f3e56dc11..00000000000 --- a/changelog/7807.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed regression in pytest 6.1.0 causing incorrect rootdir to be determined in some non-trivial cases where parent directories have config files as well. diff --git a/changelog/7814.bugfix.rst b/changelog/7814.bugfix.rst deleted file mode 100644 index a5f2a9a9518..00000000000 --- a/changelog/7814.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed crash in header reporting when :confval:`testpaths` is used and contains absolute paths (regression in 6.1.0). diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 753e81156ab..bda389cd8be 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.1.1 release-6.1.0 release-6.0.2 release-6.0.1 diff --git a/doc/en/announce/release-6.1.1.rst b/doc/en/announce/release-6.1.1.rst new file mode 100644 index 00000000000..e09408fdeea --- /dev/null +++ b/doc/en/announce/release-6.1.1.rst @@ -0,0 +1,18 @@ +pytest-6.1.1 +======================================= + +pytest 6.1.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index c620d271ff9..2a26b5c3fb9 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,18 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 6.1.1 (2020-10-03) +========================= + +Bug Fixes +--------- + +- `#7807 `_: Fixed regression in pytest 6.1.0 causing incorrect rootdir to be determined in some non-trivial cases where parent directories have config files as well. + + +- `#7814 `_: Fixed crash in header reporting when :confval:`testpaths` is used and contains absolute paths (regression in 6.1.0). + + pytest 6.1.0 (2020-09-26) ========================= diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index c8730b9a370..5a5f0fa7a43 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 6.1.0 + pytest 6.1.1 .. _`simpletest`: From 66bd44c13aa7fd1c94dd70aee40ce2d50ec20f2e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 2 Oct 2020 13:16:22 -0700 Subject: [PATCH 673/823] py36+: pyupgrade: py36+ --- .pre-commit-config.yaml | 18 ++++---- doc/en/example/assertion/failure_demo.py | 2 +- .../global_testmodule_config/conftest.py | 2 +- doc/en/example/multipython.py | 4 +- doc/en/example/nonpython/conftest.py | 2 +- scripts/release.py | 16 ++----- src/_pytest/_code/code.py | 10 ++-- src/_pytest/_io/terminalwriter.py | 4 +- src/_pytest/assertion/rewrite.py | 46 ++++++++----------- src/_pytest/assertion/truncate.py | 6 +-- src/_pytest/assertion/util.py | 10 ++-- src/_pytest/cacheprovider.py | 4 +- src/_pytest/capture.py | 4 +- src/_pytest/compat.py | 5 +- src/_pytest/config/__init__.py | 34 ++++++-------- src/_pytest/config/argparsing.py | 8 ++-- src/_pytest/debugging.py | 8 ++-- src/_pytest/doctest.py | 2 +- src/_pytest/fixtures.py | 29 ++++-------- src/_pytest/helpconfig.py | 16 +++---- src/_pytest/junitxml.py | 8 ++-- src/_pytest/main.py | 8 ++-- src/_pytest/mark/__init__.py | 6 +-- src/_pytest/mark/expression.py | 2 +- src/_pytest/mark/structures.py | 10 ++-- src/_pytest/monkeypatch.py | 8 ++-- src/_pytest/outcomes.py | 4 +- src/_pytest/pastebin.py | 2 +- src/_pytest/pathlib.py | 14 +++--- src/_pytest/pytester.py | 24 +++++----- src/_pytest/python.py | 20 ++++---- src/_pytest/python_api.py | 18 ++++---- src/_pytest/reports.py | 2 +- src/_pytest/runner.py | 12 ++--- src/_pytest/skipping.py | 4 +- src/_pytest/terminal.py | 24 +++++----- src/_pytest/tmpdir.py | 6 +-- testing/acceptance_test.py | 16 +++---- testing/code/test_excinfo.py | 14 +++--- testing/logging/test_reporting.py | 18 ++++---- testing/python/approx.py | 5 +- testing/python/fixtures.py | 8 ++-- testing/python/raises.py | 2 +- testing/test_argcomplete.py | 20 ++------ testing/test_assertion.py | 2 +- testing/test_assertrewrite.py | 12 ++--- testing/test_cacheprovider.py | 4 +- testing/test_capture.py | 2 +- testing/test_compat.py | 2 +- testing/test_config.py | 20 ++------ testing/test_debugging.py | 12 ++--- testing/test_doctest.py | 2 +- testing/test_faulthandler.py | 2 +- testing/test_helpconfig.py | 6 +-- testing/test_junitxml.py | 4 +- testing/test_link_resolve.py | 4 +- testing/test_main.py | 6 +-- testing/test_mark.py | 2 +- testing/test_meta.py | 2 +- testing/test_pytester.py | 8 ++-- testing/test_runner.py | 4 +- testing/test_session.py | 4 +- testing/test_skipping.py | 5 +- testing/test_terminal.py | 14 +++--- testing/test_unittest.py | 4 +- testing/test_warnings.py | 4 +- 66 files changed, 264 insertions(+), 346 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6068a2d324d..26289b72fd3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,12 +5,12 @@ repos: - id: black args: [--safe, --quiet] - repo: https://github.com/asottile/blacken-docs - rev: v1.7.0 + rev: v1.8.0 hooks: - id: blacken-docs additional_dependencies: [black==19.10b0] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.1.0 + rev: v3.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -21,7 +21,7 @@ repos: exclude: _pytest/(debugging|hookspec).py language_version: python3 - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.2 + rev: 3.8.3 hooks: - id: flake8 language_version: python3 @@ -29,23 +29,23 @@ repos: - flake8-typing-imports==1.9.0 - flake8-docstrings==1.5.0 - repo: https://github.com/asottile/reorder_python_imports - rev: v2.3.0 + rev: v2.3.5 hooks: - id: reorder-python-imports - args: ['--application-directories=.:src', --py3-plus] + args: ['--application-directories=.:src', --py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.4.4 + rev: v2.7.2 hooks: - id: pyupgrade - args: [--py3-plus] + args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.9.0 + rev: v1.11.0 hooks: - id: setup-cfg-fmt # TODO: when upgrading setup-cfg-fmt this can be removed args: [--max-py-version=3.9] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.780 # NOTE: keep this in sync with setup.cfg. + rev: v0.782 # NOTE: keep this in sync with setup.cfg. hooks: - id: mypy files: ^(src/|testing/) diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index a172a007e8c..abb9bce5097 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -176,7 +176,7 @@ def test_tupleerror(self): def test_reinterpret_fails_with_print_for_the_fun_of_it(self): items = [1, 2, 3] - print("items is {!r}".format(items)) + print(f"items is {items!r}") a, b = items.pop() def test_some_error(self): diff --git a/doc/en/example/assertion/global_testmodule_config/conftest.py b/doc/en/example/assertion/global_testmodule_config/conftest.py index fd467f09e59..7cdf18cdbc1 100644 --- a/doc/en/example/assertion/global_testmodule_config/conftest.py +++ b/doc/en/example/assertion/global_testmodule_config/conftest.py @@ -11,4 +11,4 @@ def pytest_runtest_setup(item): return mod = item.getparent(pytest.Module).obj if hasattr(mod, "hello"): - print("mod.hello {!r}".format(mod.hello)) + print(f"mod.hello {mod.hello!r}") diff --git a/doc/en/example/multipython.py b/doc/en/example/multipython.py index 9db6879edae..21bddcd0353 100644 --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -26,7 +26,7 @@ class Python: def __init__(self, version, picklefile): self.pythonpath = shutil.which(version) if not self.pythonpath: - pytest.skip("{!r} not found".format(version)) + pytest.skip(f"{version!r} not found") self.picklefile = picklefile def dumps(self, obj): @@ -69,4 +69,4 @@ def load_and_is_true(self, expression): @pytest.mark.parametrize("obj", [42, {}, {1: 3}]) def test_basic_objects(python1, python2, obj): python1.dumps(obj) - python2.load_and_is_true("obj == {}".format(obj)) + python2.load_and_is_true(f"obj == {obj}") diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py index 6e5a5709290..bdcc8b76222 100644 --- a/doc/en/example/nonpython/conftest.py +++ b/doc/en/example/nonpython/conftest.py @@ -40,7 +40,7 @@ def repr_failure(self, excinfo): ) def reportinfo(self): - return self.fspath, 0, "usecase: {}".format(self.name) + return self.fspath, 0, f"usecase: {self.name}" class YamlException(Exception): diff --git a/scripts/release.py b/scripts/release.py index 5e3158ab52b..798e42e1fe0 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -17,9 +17,7 @@ def announce(version): stdout = stdout.decode("utf-8") last_version = stdout.strip() - stdout = check_output( - ["git", "log", "{}..HEAD".format(last_version), "--format=%aN"] - ) + stdout = check_output(["git", "log", f"{last_version}..HEAD", "--format=%aN"]) stdout = stdout.decode("utf-8") contributors = set(stdout.splitlines()) @@ -31,14 +29,10 @@ def announce(version): Path(__file__).parent.joinpath(template_name).read_text(encoding="UTF-8") ) - contributors_text = ( - "\n".join("* {}".format(name) for name in sorted(contributors)) + "\n" - ) + contributors_text = "\n".join(f"* {name}" for name in sorted(contributors)) + "\n" text = template_text.format(version=version, contributors=contributors_text) - target = Path(__file__).parent.joinpath( - "../doc/en/announce/release-{}.rst".format(version) - ) + target = Path(__file__).parent.joinpath(f"../doc/en/announce/release-{version}.rst") target.write_text(text, encoding="UTF-8") print(f"{Fore.CYAN}[generate.announce] {Fore.RESET}Generated {target.name}") @@ -47,7 +41,7 @@ def announce(version): lines = index_path.read_text(encoding="UTF-8").splitlines() indent = " " for index, line in enumerate(lines): - if line.startswith("{}release-".format(indent)): + if line.startswith(f"{indent}release-"): new_line = indent + target.stem if line != new_line: lines.insert(index, new_line) @@ -96,7 +90,7 @@ def pre_release(version, *, skip_check_links): if not skip_check_links: check_links() - msg = "Prepare release version {}".format(version) + msg = f"Prepare release version {version}" check_call(["git", "commit", "-a", "-m", msg]) print() diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index d6140b8dc11..7054ef40724 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -58,7 +58,7 @@ def __init__(self, rawcode) -> None: if not hasattr(rawcode, "co_filename"): rawcode = getrawcode(rawcode) if not isinstance(rawcode, CodeType): - raise TypeError("not a code object: {!r}".format(rawcode)) + raise TypeError(f"not a code object: {rawcode!r}") self.filename = rawcode.co_filename self.firstlineno = rawcode.co_firstlineno - 1 self.name = rawcode.co_name @@ -747,7 +747,7 @@ def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]: else: str_repr = safeformat(value) # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)): - lines.append("{:<10} = {}".format(name, str_repr)) + lines.append(f"{name:<10} = {str_repr}") # else: # self._line("%-10s =\\" % (name,)) # # XXX @@ -1056,7 +1056,7 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None: # separate indents and source lines that are not failures: we want to # highlight the code but not the indentation, which may contain markers # such as "> assert 0" - fail_marker = "{} ".format(FormattedExcinfo.fail_marker) + fail_marker = f"{FormattedExcinfo.fail_marker} " indent_size = len(fail_marker) indents = [] # type: List[str] source_lines = [] # type: List[str] @@ -1122,7 +1122,7 @@ def toterminal(self, tw: TerminalWriter) -> None: if i != -1: msg = msg[:i] tw.write(self.path, bold=True, red=True) - tw.line(":{}: {}".format(self.lineno, msg)) + tw.line(f":{self.lineno}: {msg}") @attr.s(eq=False) @@ -1142,7 +1142,7 @@ def toterminal(self, tw: TerminalWriter) -> None: if self.args: linesofar = "" for name, value in self.args: - ns = "{} = {}".format(name, value) + ns = f"{name} = {value}" if len(ns) + len(linesofar) + 2 > tw.fullwidth: if linesofar: tw.line(linesofar) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 3dc25c6fef1..9077d41935c 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -97,7 +97,7 @@ def width_of_current_line(self) -> int: def markup(self, text: str, **markup: bool) -> str: for name in markup: if name not in self._esctable: - raise ValueError("unknown markup: {!r}".format(name)) + raise ValueError(f"unknown markup: {name!r}") if self.hasmarkup: esc = [self._esctable[name] for name, on in markup.items() if on] if esc: @@ -128,7 +128,7 @@ def sep( # N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1) fill = sepchar * N - line = "{} {} {}".format(fill, title, fill) + line = f"{fill} {title} {fill}" else: # we want len(sepchar)*N <= fullwidth # i.e. N <= fullwidth // len(sepchar) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 1b504115ce7..e23d89569b0 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -48,7 +48,7 @@ # pytest caches rewritten pycs in pycache dirs -PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version) +PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}" PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT @@ -149,7 +149,7 @@ def exec_module(self, module: types.ModuleType) -> None: ok = try_makedirs(cache_dir) if not ok: write = False - state.trace("read only directory: {}".format(cache_dir)) + state.trace(f"read only directory: {cache_dir}") cache_name = fn.name[:-3] + PYC_TAIL pyc = cache_dir / cache_name @@ -157,7 +157,7 @@ def exec_module(self, module: types.ModuleType) -> None: # to check for a cached pyc. This may not be optimal... co = _read_pyc(fn, pyc, state.trace) if co is None: - state.trace("rewriting {!r}".format(fn)) + state.trace(f"rewriting {fn!r}") source_stat, co = _rewrite_test(fn, self.config) if write: self._writing_pyc = True @@ -166,7 +166,7 @@ def exec_module(self, module: types.ModuleType) -> None: finally: self._writing_pyc = False else: - state.trace("found cached rewritten pyc for {}".format(fn)) + state.trace(f"found cached rewritten pyc for {fn}") exec(co, module.__dict__) def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool: @@ -205,20 +205,18 @@ def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool: if self._is_marked_for_rewrite(name, state): return False - state.trace("early skip of rewriting module: {}".format(name)) + state.trace(f"early skip of rewriting module: {name}") return True def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool: # always rewrite conftest files if os.path.basename(fn) == "conftest.py": - state.trace("rewriting conftest file: {!r}".format(fn)) + state.trace(f"rewriting conftest file: {fn!r}") return True if self.session is not None: if self.session.isinitpath(py.path.local(fn)): - state.trace( - "matched test file (was specified on cmdline): {!r}".format(fn) - ) + state.trace(f"matched test file (was specified on cmdline): {fn!r}") return True # modules not passed explicitly on the command line are only @@ -226,7 +224,7 @@ def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool: fn_path = PurePath(fn) for pat in self.fnpats: if fnmatch_ex(pat, fn_path): - state.trace("matched test file {!r}".format(fn)) + state.trace(f"matched test file {fn!r}") return True return self._is_marked_for_rewrite(name, state) @@ -237,9 +235,7 @@ def _is_marked_for_rewrite(self, name: str, state: "AssertionState") -> bool: except KeyError: for marked in self._must_rewrite: if name == marked or name.startswith(marked + "."): - state.trace( - "matched marked file {!r} (from {!r})".format(name, marked) - ) + state.trace(f"matched marked file {name!r} (from {marked!r})") self._marked_for_rewrite_cache[name] = True return True @@ -308,7 +304,7 @@ def _write_pyc( with atomic_write(os.fspath(pyc), mode="wb", overwrite=True) as fp: _write_pyc_fp(fp, source_stat, co) except OSError as e: - state.trace("error writing pyc file at {}: {}".format(pyc, e)) + state.trace(f"error writing pyc file at {pyc}: {e}") # we ignore any failure to write the cache file # there are many reasons, permission-denied, pycache dir being a # file etc. @@ -324,20 +320,18 @@ def _write_pyc( source_stat: os.stat_result, pyc: Path, ) -> bool: - proc_pyc = "{}.{}".format(pyc, os.getpid()) + proc_pyc = f"{pyc}.{os.getpid()}" try: fp = open(proc_pyc, "wb") except OSError as e: - state.trace( - "error writing pyc file at {}: errno={}".format(proc_pyc, e.errno) - ) + state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}") return False try: _write_pyc_fp(fp, source_stat, co) os.rename(proc_pyc, os.fspath(pyc)) except OSError as e: - state.trace("error writing pyc file at {}: {}".format(pyc, e)) + state.trace(f"error writing pyc file at {pyc}: {e}") # we ignore any failure to write the cache file # there are many reasons, permission-denied, pycache dir being a # file etc. @@ -377,7 +371,7 @@ def _read_pyc( size = stat_result.st_size data = fp.read(12) except OSError as e: - trace("_read_pyc({}): OSError {}".format(source, e)) + trace(f"_read_pyc({source}): OSError {e}") return None # Check for invalid or out of date pyc file. if ( @@ -390,7 +384,7 @@ def _read_pyc( try: co = marshal.load(fp) except Exception as e: - trace("_read_pyc({}): marshal.load error {}".format(source, e)) + trace(f"_read_pyc({source}): marshal.load error {e}") return None if not isinstance(co, types.CodeType): trace("_read_pyc(%s): not a code object" % source) @@ -982,7 +976,7 @@ def visit_BinOp(self, binop: ast.BinOp) -> Tuple[ast.Name, str]: symbol = BINOP_MAP[binop.op.__class__] left_expr, left_expl = self.visit(binop.left) right_expr, right_expl = self.visit(binop.right) - explanation = "({} {} {})".format(left_expl, symbol, right_expl) + explanation = f"({left_expl} {symbol} {right_expl})" res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) return res, explanation @@ -1007,7 +1001,7 @@ def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]: new_call = ast.Call(new_func, new_args, new_kwargs) res = self.assign(new_call) res_expl = self.explanation_param(self.display(res)) - outer_expl = "{}\n{{{} = {}\n}}".format(res_expl, res_expl, expl) + outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}" return res, outer_expl def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]: @@ -1030,7 +1024,7 @@ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: self.push_format_context() left_res, left_expl = self.visit(comp.left) if isinstance(comp.left, (ast.Compare, ast.BoolOp)): - left_expl = "({})".format(left_expl) + left_expl = f"({left_expl})" res_variables = [self.variable() for i in range(len(comp.ops))] load_names = [ast.Name(v, ast.Load()) for v in res_variables] store_names = [ast.Name(v, ast.Store()) for v in res_variables] @@ -1041,11 +1035,11 @@ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: for i, op, next_operand in it: next_res, next_expl = self.visit(next_operand) if isinstance(next_operand, (ast.Compare, ast.BoolOp)): - next_expl = "({})".format(next_expl) + next_expl = f"({next_expl})" results.append(next_res) sym = BINOP_MAP[op.__class__] syms.append(ast.Str(sym)) - expl = "{} {} {}".format(left_expl, sym, next_expl) + expl = f"{left_expl} {sym} {next_expl}" expls.append(ast.Str(expl)) res_expr = ast.Compare(left_res, [op], [next_res]) self.statements.append(ast.Assign([store_names[i]], res_expr)) diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index c572cc74461..5ba9ddca75a 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -70,10 +70,10 @@ def _truncate_explanation( truncated_line_count += 1 # Account for the part-truncated final line msg = "...Full output truncated" if truncated_line_count == 1: - msg += " ({} line hidden)".format(truncated_line_count) + msg += f" ({truncated_line_count} line hidden)" else: - msg += " ({} lines hidden)".format(truncated_line_count) - msg += ", {}".format(USAGE_MSG) + msg += f" ({truncated_line_count} lines hidden)" + msg += f", {USAGE_MSG}" truncated_explanation.extend(["", str(msg)]) return truncated_explanation diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 1d0e903cdf8..28bd13d4daf 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -142,7 +142,7 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[ left_repr = saferepr(left, maxsize=maxsize) right_repr = saferepr(right, maxsize=maxsize) - summary = "{} {} {}".format(left_repr, op, right_repr) + summary = f"{left_repr} {op} {right_repr}" explanation = None try: @@ -316,9 +316,7 @@ def _compare_eq_sequence( left_value = left[i] right_value = right[i] - explanation += [ - "At index {} diff: {!r} != {!r}".format(i, left_value, right_value) - ] + explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"] break if comparing_bytes: @@ -338,9 +336,7 @@ def _compare_eq_sequence( extra = saferepr(right[len_left]) if len_diff == 1: - explanation += [ - "{} contains one more item: {}".format(dir_with_more, extra) - ] + explanation += [f"{dir_with_more} contains one more item: {extra}"] else: explanation += [ "%s contains %d more items, first extra item: %s" diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 5a1070b7758..23feb7fbe85 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -500,7 +500,7 @@ def pytest_report_header(config: Config) -> Optional[str]: displaypath = cachedir.relative_to(config.rootpath) except ValueError: displaypath = cachedir - return "cachedir: {}".format(displaypath) + return f"cachedir: {displaypath}" return None @@ -542,5 +542,5 @@ def cacheshow(config: Config, session: Session) -> int: # print("%s/" % p.relto(basedir)) if p.is_file(): key = str(p.relative_to(basedir)) - tw.line("{} is a file of length {:d}".format(key, p.stat().st_size)) + tw.line(f"{key} is a file of length {p.stat().st_size:d}") return 0 diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 9ba0624446f..bf3c9894152 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -544,7 +544,7 @@ def __lt__(self, other: object) -> bool: return tuple(self) < tuple(other) def __repr__(self) -> str: - return "CaptureResult(out={!r}, err={!r})".format(self.out, self.err) + return f"CaptureResult(out={self.out!r}, err={self.err!r})" class MultiCapture(Generic[AnyStr]): @@ -638,7 +638,7 @@ def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: return MultiCapture( in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True) ) - raise ValueError("unknown capturing method: {!r}".format(method)) + raise ValueError(f"unknown capturing method: {method!r}") # CaptureManager and CaptureFixture diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index a7e270859fe..69ff2e0074e 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -143,8 +143,7 @@ def getfuncargnames( parameters = signature(function).parameters except (ValueError, TypeError) as e: fail( - "Could not determine arguments of {!r}: {}".format(function, e), - pytrace=False, + f"Could not determine arguments of {function!r}: {e}", pytrace=False, ) arg_names = tuple( @@ -197,7 +196,7 @@ def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]: _non_printable_ascii_translate_table = { - i: "\\x{:02x}".format(i) for i in range(128) if i not in range(32, 127) + i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127) } _non_printable_ascii_translate_table.update( {ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"} diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 9e7cdbd00e6..08d37650c10 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -144,9 +144,7 @@ def main( except ConftestImportFailure as e: exc_info = ExceptionInfo(e.excinfo) tw = TerminalWriter(sys.stderr) - tw.line( - "ImportError while loading conftest '{e.path}'.".format(e=e), red=True - ) + tw.line(f"ImportError while loading conftest '{e.path}'.", red=True) exc_info.traceback = exc_info.traceback.filter( filter_traceback_for_conftest_import_failure ) @@ -173,7 +171,7 @@ def main( except UsageError as e: tw = TerminalWriter(sys.stderr) for msg in e.args: - tw.line("ERROR: {}\n".format(msg), red=True) + tw.line(f"ERROR: {msg}\n", red=True) return ExitCode.USAGE_ERROR @@ -206,7 +204,7 @@ def filename_arg(path: str, optname: str) -> str: :optname: Name of the option. """ if os.path.isdir(path): - raise UsageError("{} must be a filename, given: {}".format(optname, path)) + raise UsageError(f"{optname} must be a filename, given: {path}") return path @@ -217,7 +215,7 @@ def directory_arg(path: str, optname: str) -> str: :optname: Name of the option. """ if not os.path.isdir(path): - raise UsageError("{} must be a directory, given: {}".format(optname, path)) + raise UsageError(f"{optname} must be a directory, given: {path}") return path @@ -583,7 +581,7 @@ def _importconftest( if path and path.relto(dirpath) or path == dirpath: assert mod not in mods mods.append(mod) - self.trace("loading conftestmodule {!r}".format(mod)) + self.trace(f"loading conftestmodule {mod!r}") self.consider_conftest(mod) return mod @@ -889,7 +887,7 @@ def __init__( _a = FILE_OR_DIR self._parser = Parser( - usage="%(prog)s [options] [{}] [{}] [...]".format(_a, _a), + usage=f"%(prog)s [options] [{_a}] [{_a}] [...]", processopt=self._processopt, ) self.pluginmanager = pluginmanager @@ -1191,9 +1189,7 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: # we don't want to prevent --help/--version to work # so just let is pass and print a warning at the end self.issue_config_time_warning( - PytestConfigWarning( - "could not load initial conftests: {}".format(e.path) - ), + PytestConfigWarning(f"could not load initial conftests: {e.path}"), stacklevel=2, ) else: @@ -1227,7 +1223,7 @@ def _checkversion(self) -> None: def _validate_config_options(self) -> None: for key in sorted(self._get_unknown_ini_keys()): - self._warn_or_fail_if_strict("Unknown config option: {}\n".format(key)) + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") def _validate_plugins(self) -> None: required_plugins = sorted(self.getini("required_plugins")) @@ -1362,7 +1358,7 @@ def _getini(self, name: str): try: description, type, default = self._parser._inidict[name] except KeyError as e: - raise ValueError("unknown configuration value: {!r}".format(name)) from e + raise ValueError(f"unknown configuration value: {name!r}") from e override_value = self._get_override_ini_value(name) if override_value is None: try: @@ -1467,8 +1463,8 @@ def getoption(self, name: str, default=notset, skip: bool = False): if skip: import pytest - pytest.skip("no {!r} option found".format(name)) - raise ValueError("no option named {!r}".format(name)) from e + pytest.skip(f"no {name!r} option found") + raise ValueError(f"no option named {name!r}") from e def getvalue(self, name: str, path=None): """Deprecated, use getoption() instead.""" @@ -1501,7 +1497,7 @@ def _warn_about_missing_assertion(self, mode: str) -> None: def _warn_about_skipped_plugins(self) -> None: for module_name, msg in self.pluginmanager.skipped_plugins: self.issue_config_time_warning( - PytestConfigWarning("skipped plugin {!r}: {}".format(module_name, msg)), + PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"), stacklevel=2, ) @@ -1554,7 +1550,7 @@ def _strtobool(val: str) -> bool: elif val in ("n", "no", "f", "false", "off", "0"): return False else: - raise ValueError("invalid truth value {!r}".format(val)) + raise ValueError(f"invalid truth value {val!r}") @lru_cache(maxsize=50) @@ -1568,7 +1564,7 @@ def parse_warning_filter( """ parts = arg.split(":") if len(parts) > 5: - raise warnings._OptionError("too many fields (max 5): {!r}".format(arg)) + raise warnings._OptionError(f"too many fields (max 5): {arg!r}") while len(parts) < 5: parts.append("") action_, message, category_, module, lineno_ = [s.strip() for s in parts] @@ -1584,7 +1580,7 @@ def parse_warning_filter( if lineno < 0: raise ValueError except (ValueError, OverflowError) as e: - raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e + raise warnings._OptionError(f"invalid lineno {lineno_!r}") from e else: lineno = 0 return action, message, category, module, lineno diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 16777587e21..3ee54a552e0 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -188,7 +188,7 @@ def __init__(self, msg: str, option: Union["Argument", str]) -> None: def __str__(self) -> str: if self.option_id: - return "option {}: {}".format(self.option_id, self.msg) + return f"option {self.option_id}: {self.msg}" else: return self.msg @@ -389,11 +389,11 @@ def __init__( def error(self, message: str) -> "NoReturn": """Transform argparse error message into UsageError.""" - msg = "{}: error: {}".format(self.prog, message) + msg = f"{self.prog}: error: {message}" if hasattr(self._parser, "_config_source_hint"): # Type ignored because the attribute is set dynamically. - msg = "{} ({})".format(msg, self._parser._config_source_hint) # type: ignore + msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore raise UsageError(self.format_usage() + msg) @@ -410,7 +410,7 @@ def parse_args( # type: ignore if arg and arg[0] == "-": lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))] for k, v in sorted(self.extra_info.items()): - lines.append(" {}: {}".format(k, v)) + lines.append(f" {k}: {v}") self.error("\n".join(lines)) getattr(parsed, FILE_OR_DIR).extend(unrecognized) return parsed diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 2b3faf8dc6c..80004f468ee 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -35,7 +35,7 @@ def _validate_usepdb_cls(value: str) -> Tuple[str, str]: modname, classname = value.split(":") except ValueError as e: raise argparse.ArgumentTypeError( - "{!r} is not in the format 'modname:classname'".format(value) + f"{value!r} is not in the format 'modname:classname'" ) from e return (modname, classname) @@ -136,7 +136,7 @@ def _import_pdb_cls(cls, capman: Optional["CaptureManager"]): except Exception as exc: value = ":".join((modname, classname)) raise UsageError( - "--pdbcls: could not import {!r}: {}".format(value, exc) + f"--pdbcls: could not import {value!r}: {exc}" ) from exc else: import pdb @@ -257,7 +257,7 @@ def _init_pdb(cls, method, *args, **kwargs): else: capturing = cls._is_capturing(capman) if capturing == "global": - tw.sep(">", "PDB {} (IO-capturing turned off)".format(method)) + tw.sep(">", f"PDB {method} (IO-capturing turned off)") elif capturing: tw.sep( ">", @@ -265,7 +265,7 @@ def _init_pdb(cls, method, *args, **kwargs): % (method, capturing), ) else: - tw.sep(">", "PDB {}".format(method)) + tw.sep(">", f"PDB {method}") _pdb = cls._import_pdb_cls(capman)(**kwargs) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index bdbbc51976a..194e5e59896 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -349,7 +349,7 @@ def repr_failure( # type: ignore[override] ] indent = ">>>" for line in example.source.splitlines(): - lines.append("??? {} {}".format(indent, line)) + lines.append(f"??? {indent} {line}") indent = "..." if isinstance(failure, doctest.DocTestFailure): lines += checker.output_difference( diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 34d038e9e1c..bf666403594 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -469,7 +469,7 @@ def function(self): """Test function object if the request has a per-function scope.""" if self.scope != "function": raise AttributeError( - "function not available in {}-scoped context".format(self.scope) + f"function not available in {self.scope}-scoped context" ) return self._pyfuncitem.obj @@ -477,9 +477,7 @@ def function(self): def cls(self): """Class (can be None) where the test function was collected.""" if self.scope not in ("class", "function"): - raise AttributeError( - "cls not available in {}-scoped context".format(self.scope) - ) + raise AttributeError(f"cls not available in {self.scope}-scoped context") clscol = self._pyfuncitem.getparent(_pytest.python.Class) if clscol: return clscol.obj @@ -498,18 +496,14 @@ def instance(self): def module(self): """Python module object where the test function was collected.""" if self.scope not in ("function", "class", "module"): - raise AttributeError( - "module not available in {}-scoped context".format(self.scope) - ) + raise AttributeError(f"module not available in {self.scope}-scoped context") return self._pyfuncitem.getparent(_pytest.python.Module).obj @property def fspath(self) -> py.path.local: """The file system path of the test module which collected this test.""" if self.scope not in ("function", "class", "module", "package"): - raise AttributeError( - "module not available in {}-scoped context".format(self.scope) - ) + raise AttributeError(f"module not available in {self.scope}-scoped context") # TODO: Remove ignore once _pyfuncitem is properly typed. return self._pyfuncitem.fspath # type: ignore @@ -757,7 +751,7 @@ def __init__( self._fixturemanager = request._fixturemanager def __repr__(self) -> str: - return "".format(self.fixturename, self._pyfuncitem) + return f"" def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._fixturedef.addfinalizer(finalizer) @@ -792,7 +786,7 @@ def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int: except ValueError: fail( "{} {}got an unexpected scope value '{}'".format( - descr, "from {} ".format(where) if where else "", scope + descr, f"from {where} " if where else "", scope ), pytrace=False, ) @@ -848,7 +842,7 @@ def formatrepr(self) -> "FixtureLookupErrorRepr": self.argname ) else: - msg = "fixture '{}' not found".format(self.argname) + msg = f"fixture '{self.argname}' not found" msg += "\n available fixtures: {}".format(", ".join(sorted(available))) msg += "\n use 'pytest --fixtures [testpath]' for help on them." @@ -882,8 +876,7 @@ def toterminal(self, tw: TerminalWriter) -> None: ) for line in lines[1:]: tw.line( - "{} {}".format(FormattedExcinfo.flow_marker, line.strip()), - red=True, + f"{FormattedExcinfo.flow_marker} {line.strip()}", red=True, ) tw.line() tw.line("%s:%d" % (self.filename, self.firstlineno + 1)) @@ -907,9 +900,7 @@ def call_fixture_func( try: fixture_result = next(generator) except StopIteration: - raise ValueError( - "{} did not yield a value".format(request.fixturename) - ) from None + raise ValueError(f"{request.fixturename} did not yield a value") from None finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator) request.addfinalizer(finalizer) else: @@ -987,7 +978,7 @@ def __init__( self.scopenum = scope2index( # TODO: Check if the `or` here is really necessary. scope_ or "function", # type: ignore[unreachable] - descr="Fixture '{}'".format(func.__name__), + descr=f"Fixture '{func.__name__}'", where=baseid, ) self.scope = scope_ diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 348a65edec6..9c3a1804d5f 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -137,7 +137,7 @@ def showversion(config: Config) -> None: for line in plugininfo: sys.stderr.write(line + "\n") else: - sys.stderr.write("pytest {}\n".format(pytest.__version__)) + sys.stderr.write(f"pytest {pytest.__version__}\n") def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: @@ -172,8 +172,8 @@ def showhelp(config: Config) -> None: if type is None: type = "string" if help is None: - raise TypeError("help argument cannot be None for {}".format(name)) - spec = "{} ({}):".format(name, type) + raise TypeError(f"help argument cannot be None for {name}") + spec = f"{name} ({type}):" tw.write(" %s" % spec) spec_len = len(spec) if spec_len > (indent_len - 3): @@ -208,7 +208,7 @@ def showhelp(config: Config) -> None: ("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals"), ] for name, help in vars: - tw.line(" {:<24} {}".format(name, help)) + tw.line(f" {name:<24} {help}") tw.line() tw.line() @@ -235,7 +235,7 @@ def getpluginversioninfo(config: Config) -> List[str]: lines.append("setuptools registered plugins:") for plugin, dist in plugininfo: loc = getattr(plugin, "__file__", repr(plugin)) - content = "{}-{} at {}".format(dist.project_name, dist.version, loc) + content = f"{dist.project_name}-{dist.version} at {loc}" lines.append(" " + content) return lines @@ -243,9 +243,7 @@ def getpluginversioninfo(config: Config) -> List[str]: def pytest_report_header(config: Config) -> List[str]: lines = [] if config.option.debug or config.option.traceconfig: - lines.append( - "using: pytest-{} pylib-{}".format(pytest.__version__, py.__version__) - ) + lines.append(f"using: pytest-{pytest.__version__} pylib-{py.__version__}") verinfo = getpluginversioninfo(config) if verinfo: @@ -259,5 +257,5 @@ def pytest_report_header(config: Config) -> List[str]: r = plugin.__file__ else: r = repr(plugin) - lines.append(" {:<20}: {}".format(name, r)) + lines.append(f" {name:<20}: {r}") return lines diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 877b9be78bc..621d631768b 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -228,9 +228,9 @@ def append_error(self, report: TestReport) -> None: reason = str(report.longrepr) if report.when == "teardown": - msg = 'failed on teardown with "{}"'.format(reason) + msg = f'failed on teardown with "{reason}"' else: - msg = 'failed on setup with "{}"'.format(reason) + msg = f'failed on setup with "{reason}"' self._add_simple("error", msg, str(report.longrepr)) def append_skipped(self, report: TestReport) -> None: @@ -246,7 +246,7 @@ def append_skipped(self, report: TestReport) -> None: filename, lineno, skipreason = report.longrepr if skipreason.startswith("Skipped: "): skipreason = skipreason[9:] - details = "{}:{}: {}".format(filename, lineno, skipreason) + details = f"{filename}:{lineno}: {skipreason}" skipped = ET.Element("skipped", type="pytest.skip", message=skipreason) skipped.text = bin_xml_escape(details) @@ -683,7 +683,7 @@ def pytest_sessionfinish(self) -> None: logfile.close() def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None: - terminalreporter.write_sep("-", "generated xml file: {}".format(self.logfile)) + terminalreporter.write_sep("-", f"generated xml file: {self.logfile}") def add_global_property(self, name: str, value: object) -> None: __tracebackhide__ = True diff --git a/src/_pytest/main.py b/src/_pytest/main.py index bb11df4eab8..bb08bb15c16 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -267,9 +267,7 @@ def wrap_session( if excinfo.value.returncode is not None: exitstatus = excinfo.value.returncode if initstate < 2: - sys.stderr.write( - "{}: {}\n".format(excinfo.typename, excinfo.value.msg) - ) + sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n") config.hook.pytest_keyboard_interrupt(excinfo=excinfo) session.exitstatus = exitstatus except BaseException: @@ -615,8 +613,8 @@ def perform_collect( if self._notfound: errors = [] for arg, cols in self._notfound: - line = "(no name {!r} in any of {!r})".format(arg, cols) - errors.append("not found: {}\n{}".format(arg, line)) + line = f"(no name {arg!r} in any of {cols!r})" + errors.append(f"not found: {arg}\n{line}") raise UsageError(*errors) if not genitems: items = rep.result diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index cc6e80e8015..329a11c4ae8 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -201,7 +201,7 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None: expression = Expression.compile(keywordexpr) except ParseError as e: raise UsageError( - "Wrong expression passed to '-k': {}: {}".format(keywordexpr, e) + f"Wrong expression passed to '-k': {keywordexpr}: {e}" ) from None remaining = [] @@ -245,9 +245,7 @@ def deselect_by_mark(items: "List[Item]", config: Config) -> None: try: expression = Expression.compile(matchexpr) except ParseError as e: - raise UsageError( - "Wrong expression passed to '-m': {}: {}".format(matchexpr, e) - ) from None + raise UsageError(f"Wrong expression passed to '-m': {matchexpr}: {e}") from None remaining = [] deselected = [] diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 9f4592221b9..b3acef5d010 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -66,7 +66,7 @@ def __init__(self, column: int, message: str) -> None: self.message = message def __str__(self) -> str: - return "at column {}: {}".format(self.column, self.message) + return f"at column {self.column}: {self.message}" class Scanner: diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 0af996fd0e5..b2ab2e35be1 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -310,7 +310,7 @@ def markname(self) -> str: return self.name # for backward-compat (2.4.1 had this attr) def __repr__(self) -> str: - return "".format(self.mark) + return f"" def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": """Return a MarkDecorator with extra arguments added. @@ -364,7 +364,7 @@ def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List ] # unpack MarkDecorator for mark in extracted: if not isinstance(mark, Mark): - raise TypeError("got {!r} instead of Mark".format(mark)) + raise TypeError(f"got {mark!r} instead of Mark") return [x for x in extracted if isinstance(x, Mark)] @@ -498,14 +498,14 @@ def __getattr__(self, name: str) -> MarkDecorator: if name not in self._markers: if self._config.option.strict_markers: fail( - "{!r} not found in `markers` configuration option".format(name), + f"{name!r} not found in `markers` configuration option", pytrace=False, ) # Raise a specific error for common misspellings of "parametrize". if name in ["parameterize", "parametrise", "parameterise"]: __tracebackhide__ = True - fail("Unknown '{}' mark, did you mean 'parametrize'?".format(name)) + fail(f"Unknown '{name}' mark, did you mean 'parametrize'?") warnings.warn( "Unknown pytest.mark.%s - is this a typo? You can register " @@ -556,4 +556,4 @@ def __len__(self) -> int: return len(self._seen()) def __repr__(self) -> str: - return "".format(self.node) + return f"" diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index bca12cd7a17..d75032e65a2 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -74,7 +74,7 @@ def resolve(name: str) -> object: if expected == used: raise else: - raise ImportError("import error in {}: {}".format(used, ex)) from ex + raise ImportError(f"import error in {used}: {ex}") from ex found = annotated_getattr(found, part, used) return found @@ -93,9 +93,7 @@ def annotated_getattr(obj: object, name: str, ann: str) -> object: def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]: if not isinstance(import_path, str) or "." not in import_path: # type: ignore[unreachable] - raise TypeError( - "must be absolute import path string, not {!r}".format(import_path) - ) + raise TypeError(f"must be absolute import path string, not {import_path!r}") module, attr = import_path.rsplit(".", 1) target = resolve(module) if raising: @@ -202,7 +200,7 @@ def setattr( oldval = getattr(target, name, notset) if raising and oldval is notset: - raise AttributeError("{!r} has no attribute {!r}".format(target, name)) + raise AttributeError(f"{target!r} has no attribute {name!r}") # avoid class descriptors like staticmethod/classmethod if inspect.isclass(target): diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index cc70e72d4b1..8130a441367 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -40,7 +40,7 @@ def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None: def __repr__(self) -> str: if self.msg: return self.msg - return "<{} instance>".format(self.__class__.__name__) + return f"<{self.__class__.__name__} instance>" __str__ = __repr__ @@ -208,7 +208,7 @@ def importorskip( __import__(modname) except ImportError as exc: if reason is None: - reason = "could not import {!r}: {}".format(modname, exc) + reason = f"could not import {modname!r}: {exc}" raise Skipped(reason, allow_module_level=True) from None mod = sys.modules[modname] if minversion is None: diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 0546d237762..c206900db95 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -107,4 +107,4 @@ def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None: s = file.getvalue() assert len(s) pastebinurl = create_new_paste(s) - terminalreporter.write_line("{} --> {}".format(msg, pastebinurl)) + terminalreporter.write_line(f"{msg} --> {pastebinurl}") diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 75663ee6293..0bc5bff2bb5 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -63,9 +63,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool: if not isinstance(excvalue, PermissionError): warnings.warn( - PytestWarning( - "(rm_rf) error removing {}\n{}: {}".format(path, exctype, excvalue) - ) + PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}") ) return False @@ -200,7 +198,7 @@ def make_numbered_dir(root: Path, prefix: str) -> Path: # try up to 10 times to create the folder max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) new_number = max_existing + 1 - new_path = root.joinpath("{}{}".format(prefix, new_number)) + new_path = root.joinpath(f"{prefix}{new_number}") try: new_path.mkdir() except Exception: @@ -221,7 +219,7 @@ def create_cleanup_lock(p: Path) -> Path: try: fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) except FileExistsError as e: - raise OSError("cannot create lockfile in {path}".format(path=p)) from e + raise OSError(f"cannot create lockfile in {p}") from e else: pid = os.getpid() spid = str(pid).encode() @@ -258,7 +256,7 @@ def maybe_delete_a_numbered_dir(path: Path) -> None: lock_path = create_cleanup_lock(path) parent = path.parent - garbage = parent.joinpath("garbage-{}".format(uuid.uuid4())) + garbage = parent.joinpath(f"garbage-{uuid.uuid4()}") path.rename(garbage) rm_rf(garbage) except OSError: @@ -401,7 +399,7 @@ def fnmatch_ex(pattern: str, path) -> bool: else: name = str(path) if path.is_absolute() and not os.path.isabs(pattern): - pattern = "*{}{}".format(os.sep, pattern) + pattern = f"*{os.sep}{pattern}" return fnmatch.fnmatch(name, pattern) @@ -415,7 +413,7 @@ def symlink_or_skip(src, dst, **kwargs): try: os.symlink(str(src), str(dst), **kwargs) except OSError as e: - skip("symlinks not supported: {}".format(e)) + skip(f"symlinks not supported: {e}") class ImportMode(Enum): diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 834069a09b4..0a4fffed7ae 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -197,7 +197,7 @@ def __init__(self, name: str, kwargs) -> None: def __repr__(self) -> str: d = self.__dict__.copy() del d["_name"] - return "".format(self._name, d) + return f"" if TYPE_CHECKING: # The class has undetermined attributes, this tells mypy about it. @@ -252,7 +252,7 @@ def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None: break print("NONAMEMATCH", name, "with", call) else: - pytest.fail("could not find {!r} check {!r}".format(name, check)) + pytest.fail(f"could not find {name!r} check {check!r}") def popcall(self, name: str) -> ParsedCall: __tracebackhide__ = True @@ -260,7 +260,7 @@ def popcall(self, name: str) -> ParsedCall: if call._name == name: del self.calls[i] return call - lines = ["could not find call {!r}, in:".format(name)] + lines = [f"could not find call {name!r}, in:"] lines.extend([" %s" % x for x in self.calls]) pytest.fail("\n".join(lines)) @@ -388,7 +388,7 @@ def listoutcomes( elif rep.skipped: skipped.append(rep) else: - assert rep.failed, "Unexpected outcome: {!r}".format(rep) + assert rep.failed, f"Unexpected outcome: {rep!r}" failed.append(rep) return passed, skipped, failed @@ -658,7 +658,7 @@ def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> N mp.setenv("PY_COLORS", "0") def __repr__(self) -> str: - return "".format(self.tmpdir) + return f"" def __str__(self) -> str: return str(self.tmpdir) @@ -874,7 +874,7 @@ def copy_example(self, name=None) -> py.path.local: return result else: raise LookupError( - 'example "{}" is not found as a file or directory'.format(example_path) + f'example "{example_path}" is not found as a file or directory' ) Session = Session @@ -1087,7 +1087,7 @@ def runpytest(self, *args, **kwargs) -> RunResult: return self.runpytest_inprocess(*args, **kwargs) elif self._method == "subprocess": return self.runpytest_subprocess(*args, **kwargs) - raise RuntimeError("Unrecognized runpytest option: {}".format(self._method)) + raise RuntimeError(f"Unrecognized runpytest option: {self._method}") def _ensure_basetemp(self, args): args = list(args) @@ -1329,7 +1329,7 @@ def _dump_lines(self, lines, fp): for line in lines: print(line, file=fp) except UnicodeEncodeError: - print("couldn't print to {} because of encoding".format(fp)) + print(f"couldn't print to {fp} because of encoding") def _getpytestargs(self) -> Tuple[str, ...]: return sys.executable, "-mpytest" @@ -1386,7 +1386,7 @@ def spawn_pytest( """ basetemp = self.tmpdir.mkdir("temp-pexpect") invoke = " ".join(map(str, self._getpytestargs())) - cmd = "{} --basetemp={} {}".format(invoke, basetemp, string) + cmd = f"{invoke} --basetemp={basetemp} {string}" return self.spawn(cmd, expect_timeout=expect_timeout) def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": @@ -1573,7 +1573,7 @@ def _match_lines( break else: if consecutive and started: - msg = "no consecutive match: {!r}".format(line) + msg = f"no consecutive match: {line!r}" self._log(msg) self._log( "{:>{width}}".format("with:", width=wnick), repr(nextline) @@ -1587,7 +1587,7 @@ def _match_lines( self._log("{:>{width}}".format("and:", width=wnick), repr(nextline)) extralines.append(nextline) else: - msg = "remains unmatched: {!r}".format(line) + msg = f"remains unmatched: {line!r}" self._log(msg) self._fail(msg) self._log_output = [] @@ -1622,7 +1622,7 @@ def _no_match_line( wnick = len(match_nickname) + 1 for line in self.lines: if match_func(line, pat): - msg = "{}: {!r}".format(match_nickname, pat) + msg = f"{match_nickname}: {pat!r}" self._log(msg) self._log("{:>{width}}".format("with:", width=wnick), repr(line)) self._fail(msg) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 1acf2c0e5a3..d07615a5a71 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -483,7 +483,7 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]: fixtureinfo.prune_dependency_tree() for callspec in metafunc._calls: - subname = "{}[{}]".format(name, callspec.id) + subname = f"{name}[{callspec.id}]" yield Function.from_parent( self, name=subname, @@ -888,7 +888,7 @@ def copy(self) -> "CallSpec2": def _checkargnotcontained(self, arg: str) -> None: if arg in self.params or arg in self.funcargs: - raise ValueError("duplicate {!r}".format(arg)) + raise ValueError(f"duplicate {arg!r}") def getparam(self, name: str) -> object: try: @@ -918,7 +918,7 @@ def setmulti2( elif valtype_for_arg == "funcargs": self.funcargs[arg] = val else: # pragma: no cover - assert False, "Unhandled valtype for arg: {}".format(valtype_for_arg) + assert False, f"Unhandled valtype for arg: {valtype_for_arg}" self.indices[arg] = param_index self._arg2scopenum[arg] = scopenum self._idlist.append(id) @@ -1068,7 +1068,7 @@ def parametrize( object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids) scopenum = scope2index( - scope, descr="parametrize() call in {}".format(self.function.__name__) + scope, descr=f"parametrize() call in {self.function.__name__}" ) # Create the new calls: if we are parametrize() multiple times (by applying the decorator @@ -1224,7 +1224,7 @@ def _validate_if_using_arg_names( else: name = "fixture" if indirect else "argument" fail( - "In {}: function uses no {} '{}'".format(func_name, name, arg), + f"In {func_name}: function uses no {name} '{arg}'", pytrace=False, ) @@ -1291,7 +1291,7 @@ def _idval( if generated_id is not None: val = generated_id except Exception as e: - prefix = "{}: ".format(nodeid) if nodeid is not None else "" + prefix = f"{nodeid}: " if nodeid is not None else "" msg = "error raised while trying to determine id of parameter '{}' at position {}" msg = prefix + msg.format(argname, idx) raise ValueError(msg) from e @@ -1400,7 +1400,7 @@ def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: return if verbose > 0: bestrel = get_best_relpath(fixture_def.func) - funcargspec = "{} -- {}".format(argname, bestrel) + funcargspec = f"{argname} -- {bestrel}" else: funcargspec = argname tw.line(funcargspec, green=True) @@ -1417,7 +1417,7 @@ def write_item(item: nodes.Item) -> None: # This test item does not use any fixtures. return tw.line() - tw.sep("-", "fixtures used by {}".format(item.name)) + tw.sep("-", f"fixtures used by {item.name}") # TODO: Fix this type ignore. tw.sep("-", "({})".format(get_best_relpath(item.function))) # type: ignore[attr-defined] # dict key not used in loop but needed for sorting. @@ -1476,7 +1476,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: if currentmodule != module: if not module.startswith("_pytest."): tw.line() - tw.sep("-", "fixtures defined from {}".format(module)) + tw.sep("-", f"fixtures defined from {module}") currentmodule = module if verbose <= 0 and argname[0] == "_": continue @@ -1491,7 +1491,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: if doc: write_docstring(tw, doc) else: - tw.line(" {}: no docstring available".format(loc), red=True) + tw.line(f" {loc}: no docstring available", red=True) tw.line() diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index e00a1b25da7..9d19c073c8e 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -25,7 +25,7 @@ def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: - at_str = " at {}".format(at) if at else "" + at_str = f" at {at}" if at else "" return TypeError( "cannot make approximate comparisons to non-numeric values: {!r} {}".format( value, at_str @@ -98,7 +98,7 @@ class ApproxNumpy(ApproxBase): def __repr__(self) -> str: list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist()) - return "approx({!r})".format(list_scalars) + return f"approx({list_scalars!r})" def __eq__(self, actual) -> bool: import numpy as np @@ -109,9 +109,7 @@ def __eq__(self, actual) -> bool: try: actual = np.asarray(actual) except Exception as e: - raise TypeError( - "cannot compare '{}' to numpy.ndarray".format(actual) - ) from e + raise TypeError(f"cannot compare '{actual}' to numpy.ndarray") from e if not np.isscalar(actual) and actual.shape != self.expected.shape: return False @@ -219,7 +217,7 @@ def __repr__(self) -> str: # If a sensible tolerance can't be calculated, self.tolerance will # raise a ValueError. In this case, display '???'. try: - vetted_tolerance = "{:.1e}".format(self.tolerance) + vetted_tolerance = f"{self.tolerance:.1e}" if ( isinstance(self.expected, Complex) and self.expected.imag @@ -229,7 +227,7 @@ def __repr__(self) -> str: except ValueError: vetted_tolerance = "???" - return "{} ± {}".format(self.expected, vetted_tolerance) + return f"{self.expected} ± {vetted_tolerance}" def __eq__(self, actual) -> bool: """Return whether the given value is equal to the expected value @@ -291,7 +289,7 @@ def set_default(x, default): if absolute_tolerance < 0: raise ValueError( - "absolute tolerance can't be negative: {}".format(absolute_tolerance) + f"absolute tolerance can't be negative: {absolute_tolerance}" ) if math.isnan(absolute_tolerance): raise ValueError("absolute tolerance can't be NaN.") @@ -313,7 +311,7 @@ def set_default(x, default): if relative_tolerance < 0: raise ValueError( - "relative tolerance can't be negative: {}".format(absolute_tolerance) + f"relative tolerance can't be negative: {absolute_tolerance}" ) if math.isnan(relative_tolerance): raise ValueError("relative tolerance can't be NaN.") @@ -698,7 +696,7 @@ def raises( not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__ raise TypeError(msg.format(not_a)) - message = "DID NOT RAISE {}".format(expected_exception) + message = f"DID NOT RAISE {expected_exception}" if not args: match = kwargs.pop("match", None) # type: Optional[Union[str, Pattern[str]]] diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index c60137d3888..8e7f0f9bba9 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -321,7 +321,7 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": excinfo, style=item.config.getoption("tbstyle", "auto") ) for rwhen, key, content in item._report_sections: - sections.append(("Captured {} {}".format(key, rwhen), content)) + sections.append((f"Captured {key} {rwhen}", content)) return cls( item.nodeid, item.location, diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index e16fb4ab477..e617c232640 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -93,7 +93,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: % (len(dlist) - i, durations_min) ) break - tr.write_line("{:02.2f}s {:<8} {}".format(rep.duration, rep.when, rep.nodeid)) + tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}") def pytest_sessionstart(session: "Session") -> None: @@ -186,7 +186,7 @@ def _update_current_test_var( """ var_name = "PYTEST_CURRENT_TEST" if when: - value = "{} ({})".format(item.nodeid, when) + value = f"{item.nodeid} ({when})" # don't allow null bytes on environment variables (see #2644, #2957) value = value.replace("\x00", "(null)") os.environ[var_name] = value @@ -248,7 +248,7 @@ def call_runtest_hook( elif when == "teardown": ihook = item.ihook.pytest_runtest_teardown else: - assert False, "Unhandled runtest hook case: {}".format(when) + assert False, f"Unhandled runtest hook case: {when}" reraise = (Exit,) # type: Tuple[Type[BaseException], ...] if not item.config.getoption("usepdb", False): reraise += (KeyboardInterrupt,) @@ -290,7 +290,7 @@ class CallInfo(Generic[TResult]): @property def result(self) -> TResult: if self.excinfo is not None: - raise AttributeError("{!r} has no valid result".format(self)) + raise AttributeError(f"{self!r} has no valid result") # The cast is safe because an exception wasn't raised, hence # _result has the expected function return type (which may be # None, that's why a cast and not an assert). @@ -330,8 +330,8 @@ def from_call( def __repr__(self) -> str: if self.excinfo is None: - return "".format(self.when, self._result) - return "".format(self.when, self.excinfo) + return f"" + return f"" def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index cc505fdd7c2..afc3610eb4c 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -101,7 +101,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, if hasattr(item, "obj"): globals_.update(item.obj.__globals__) # type: ignore[attr-defined] try: - filename = "<{} condition>".format(mark.name) + filename = f"<{mark.name} condition>" condition_code = compile(condition, filename, "eval") result = eval(condition_code, globals_) except SyntaxError as exc: @@ -264,7 +264,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): if unexpectedsuccess_key in item._store and rep.when == "call": reason = item._store[unexpectedsuccess_key] if reason: - rep.longrepr = "Unexpected success: {}".format(reason) + rep.longrepr = f"Unexpected success: {reason}" else: rep.longrepr = "Unexpected success" rep.outcome = "failed" diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 5b7a09c6435..f84797af29e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -304,7 +304,7 @@ def get_location(self, config: Config) -> Optional[str]: relpath = bestrelpath( config.invocation_params.dir, absolutepath(filename) ) - return "{}:{}".format(relpath, linenum) + return f"{relpath}:{linenum}" else: return str(self.fslocation) return None @@ -487,7 +487,7 @@ def pytest_warning_recorded( def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: if self.config.option.traceconfig: - msg = "PLUGIN registered: {}".format(plugin) + msg = f"PLUGIN registered: {plugin}" # XXX This event may happen during setup/teardown time # which unfortunately captures our output here # which garbles our output if we use self.write_line. @@ -593,9 +593,9 @@ def _get_progress_information_message(self) -> str: if collected: progress = self._progress_nodeids_reported counter_format = "{{:{}d}}".format(len(str(collected))) - format_string = " [{}/{{}}]".format(counter_format) + format_string = f" [{counter_format}/{{}}]" return format_string.format(len(progress), collected) - return " [ {} / {} ]".format(collected, collected) + return f" [ {collected} / {collected} ]" else: if collected: return " [{:3d}%]".format( @@ -682,7 +682,7 @@ def pytest_sessionstart(self, session: "Session") -> None: self.write_sep("=", "test session starts", bold=True) verinfo = platform.python_version() if not self.no_header: - msg = "platform {} -- Python {}".format(sys.platform, verinfo) + msg = f"platform {sys.platform} -- Python {verinfo}" pypy_version_info = getattr(sys, "pypy_version_info", None) if pypy_version_info: verinfo = ".".join(map(str, pypy_version_info[:3])) @@ -778,7 +778,7 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: if col.name == "()": # Skip Instances. continue indent = (len(stack) - 1) * " " - self._tw.line("{}{}".format(indent, col)) + self._tw.line(f"{indent}{col}") if self.config.option.verbose >= 1: obj = getattr(col, "obj", None) doc = inspect.getdoc(obj) if obj else None @@ -1018,7 +1018,7 @@ def summary_errors(self) -> None: if rep.when == "collect": msg = "ERROR collecting " + msg else: - msg = "ERROR at {} of {}".format(rep.when, msg) + msg = f"ERROR at {rep.when} of {msg}" self.write_sep("_", msg, red=True, bold=True) self._outrep_summary(rep) @@ -1091,7 +1091,7 @@ def show_xfailed(lines: List[str]) -> None: for rep in xfailed: verbose_word = rep._get_verbose_word(self.config) pos = _get_pos(self.config, rep) - lines.append("{} {}".format(verbose_word, pos)) + lines.append(f"{verbose_word} {pos}") reason = rep.wasxfail if reason: lines.append(" " + str(reason)) @@ -1102,7 +1102,7 @@ def show_xpassed(lines: List[str]) -> None: verbose_word = rep._get_verbose_word(self.config) pos = _get_pos(self.config, rep) reason = rep.wasxfail - lines.append("{} {} {}".format(verbose_word, pos, reason)) + lines.append(f"{verbose_word} {pos} {reason}") def show_skipped(lines: List[str]) -> None: skipped = self.stats.get("skipped", []) # type: List[CollectReport] @@ -1201,7 +1201,7 @@ def _get_line_with_reprcrash_message( verbose_word = rep._get_verbose_word(config) pos = _get_pos(config, rep) - line = "{} {}".format(verbose_word, pos) + line = f"{verbose_word} {pos}" len_line = wcswidth(line) ellipsis, len_ellipsis = "...", 3 if len_line > termwidth - len_ellipsis: @@ -1302,7 +1302,7 @@ def _plugin_nameversions(plugininfo) -> List[str]: def format_session_duration(seconds: float) -> str: """Format the given seconds in a human readable manner to show in the final summary.""" if seconds < 60: - return "{:.2f}s".format(seconds) + return f"{seconds:.2f}s" else: dt = datetime.timedelta(seconds=int(seconds)) - return "{:.2f}s ({})".format(seconds, dt) + return f"{seconds:.2f}s ({dt})" diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 02a613483a3..b889be88897 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -50,9 +50,7 @@ def from_config(cls, config: Config) -> "TempPathFactory": def _ensure_relative_to_basetemp(self, basename: str) -> str: basename = os.path.normpath(basename) if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp(): - raise ValueError( - "{} is not a normalized and relative path".format(basename) - ) + raise ValueError(f"{basename} is not a normalized and relative path") return basename def mktemp(self, basename: str, numbered: bool = True) -> Path: @@ -94,7 +92,7 @@ def getbasetemp(self) -> Path: user = get_user() or "unknown" # use a sub-directory in the temproot to speed-up # make_numbered_dir() call - rootdir = temproot.joinpath("pytest-of-{}".format(user)) + rootdir = temproot.joinpath(f"pytest-of-{user}") rootdir.mkdir(exist_ok=True) basetemp = make_numbered_dir_with_cleanup( prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ff5112d55ed..f4b7d6135ed 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -156,7 +156,7 @@ def test_this(): assert x """ ) - result = testdir.runpytest(p, "--import-mode={}".format(import_mode)) + result = testdir.runpytest(p, f"--import-mode={import_mode}") result.stdout.fnmatch_lines(["> assert x", "E assert 0"]) assert result.ret == 1 @@ -185,7 +185,7 @@ def test_not_collectable_arguments(self, testdir): assert result.ret == ExitCode.USAGE_ERROR result.stderr.fnmatch_lines( [ - "ERROR: not found: {}".format(p2), + f"ERROR: not found: {p2}", "(no name {!r} in any of [[][]])".format(str(p2)), "", ] @@ -212,7 +212,7 @@ def foo(): result = testdir.runpytest() assert result.stdout.lines == [] assert result.stderr.lines == [ - "ImportError while loading conftest '{}'.".format(conftest), + f"ImportError while loading conftest '{conftest}'.", "conftest.py:3: in ", " foo()", "conftest.py:2: in foo", @@ -503,7 +503,7 @@ def test_earlyinit(self, testdir): def test_pydoc(self, testdir): for name in ("py.test", "pytest"): - result = testdir.runpython_c("import {};help({})".format(name, name)) + result = testdir.runpython_c(f"import {name};help({name})") assert result.ret == 0 s = result.stdout.str() assert "MarkGenerator" in s @@ -671,8 +671,8 @@ def test_cmdline_python_namespace_package(self, testdir, monkeypatch): ) lib = ns.mkdir(dirname) lib.ensure("__init__.py") - lib.join("test_{}.py".format(dirname)).write( - "def test_{}(): pass\ndef test_other():pass".format(dirname) + lib.join(f"test_{dirname}.py").write( + f"def test_{dirname}(): pass\ndef test_other():pass" ) # The structure of the test directory is now: @@ -891,7 +891,7 @@ def test_calls_showall(self, testdir, mock_timing): if ("test_%s" % x) in line and y in line: break else: - raise AssertionError("not found {} {}".format(x, y)) + raise AssertionError(f"not found {x} {y}") def test_calls_showall_verbose(self, testdir, mock_timing): testdir.makepyfile(self.source) @@ -904,7 +904,7 @@ def test_calls_showall_verbose(self, testdir, mock_timing): if ("test_%s" % x) in line and y in line: break else: - raise AssertionError("not found {} {}".format(x, y)) + raise AssertionError(f"not found {x} {y}") def test_with_deselected(self, testdir, mock_timing): testdir.makepyfile(self.source) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index ce07d4dbcce..d1ed43300db 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -206,8 +206,8 @@ def h(): excinfo = pytest.raises(ValueError, h) traceback = excinfo.traceback ntraceback = traceback.filter() - print("old: {!r}".format(traceback)) - print("new: {!r}".format(ntraceback)) + print(f"old: {traceback!r}") + print(f"new: {ntraceback!r}") if matching: assert len(ntraceback) == len(traceback) - 2 @@ -265,7 +265,7 @@ def test_traceback_messy_recursion(self): decorator = pytest.importorskip("decorator").decorator def log(f, *k, **kw): - print("{} {}".format(k, kw)) + print(f"{k} {kw}") f(*k, **kw) log = decorator(log) @@ -426,13 +426,13 @@ def test_division_zero(): assert result.ret != 0 exc_msg = "Regex pattern '[[]123[]]+' does not match 'division by zero'." - result.stdout.fnmatch_lines(["E * AssertionError: {}".format(exc_msg)]) + result.stdout.fnmatch_lines([f"E * AssertionError: {exc_msg}"]) result.stdout.no_fnmatch_line("*__tracebackhide__ = True*") result = testdir.runpytest("--fulltrace") assert result.ret != 0 result.stdout.fnmatch_lines( - ["*__tracebackhide__ = True*", "E * AssertionError: {}".format(exc_msg)] + ["*__tracebackhide__ = True*", f"E * AssertionError: {exc_msg}"] ) @@ -834,14 +834,14 @@ def raiseos(): "def entry():", "> f(0)", "", - "{}:5: ".format(mod.__file__), + f"{mod.__file__}:5: ", "_ _ *", "", " def f(x):", "> raise ValueError(x)", "E ValueError: 0", "", - "{}:3: ValueError".format(mod.__file__), + f"{mod.__file__}:3: ValueError", ] ) assert raised == 3 diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 7590b576289..7545d016d52 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -262,10 +262,10 @@ def test_log_2(): result = testdir.runpytest() result.stdout.fnmatch_lines( [ - "{}::test_log_1 ".format(filename), + f"{filename}::test_log_1 ", "*WARNING*log message from test_log_1*", "PASSED *50%*", - "{}::test_log_2 ".format(filename), + f"{filename}::test_log_2 ", "*WARNING*log message from test_log_2*", "PASSED *100%*", "=* 2 passed in *=", @@ -318,7 +318,7 @@ def test_log_2(fix): result = testdir.runpytest() result.stdout.fnmatch_lines( [ - "{}::test_log_1 ".format(filename), + f"{filename}::test_log_1 ", "*-- live log start --*", "*WARNING* >>>>> START >>>>>*", "*-- live log setup --*", @@ -330,7 +330,7 @@ def test_log_2(fix): "*WARNING*log message from teardown of test_log_1*", "*-- live log finish --*", "*WARNING* <<<<< END <<<<<<<*", - "{}::test_log_2 ".format(filename), + f"{filename}::test_log_2 ", "*-- live log start --*", "*WARNING* >>>>> START >>>>>*", "*-- live log setup --*", @@ -394,7 +394,7 @@ def test_log_1(fix): result.stdout.fnmatch_lines( [ "*WARNING*Unknown Section*", - "{}::test_log_1 ".format(filename), + f"{filename}::test_log_1 ", "*WARNING* >>>>> START >>>>>*", "*-- live log setup --*", "*WARNING*log message from setup of test_log_1*", @@ -453,7 +453,7 @@ def test_log_1(fix): result = testdir.runpytest() result.stdout.fnmatch_lines( [ - "{}::test_log_1 ".format(filename), + f"{filename}::test_log_1 ", "*-- live log start --*", "*WARNING* >>>>> START >>>>>*", "*-- live log setup --*", @@ -638,7 +638,7 @@ def test_log_file(request): log_file = testdir.tmpdir.join("pytest.log").strpath result = testdir.runpytest( - "-s", "--log-file={}".format(log_file), "--log-file-level=WARNING" + "-s", f"--log-file={log_file}", "--log-file-level=WARNING" ) # fnmatch_lines does an assertion internally @@ -670,9 +670,7 @@ def test_log_file(request): log_file = testdir.tmpdir.join("pytest.log").strpath - result = testdir.runpytest( - "-s", "--log-file={}".format(log_file), "--log-file-level=INFO" - ) + result = testdir.runpytest("-s", f"--log-file={log_file}", "--log-file-level=INFO") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines(["test_log_file_cli_level.py PASSED"]) diff --git a/testing/python/approx.py b/testing/python/approx.py index 5f12da37654..b37c9f757d0 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -471,7 +471,7 @@ def test_foo(): expected = "4.0e-06" result = testdir.runpytest() result.stdout.fnmatch_lines( - ["*At index 0 diff: 3 != 4 ± {}".format(expected), "=* 1 failed in *="] + [f"*At index 0 diff: 3 != 4 ± {expected}", "=* 1 failed in *="] ) @pytest.mark.parametrize( @@ -483,8 +483,7 @@ def test_foo(): ) def test_expected_value_type_error(self, x, name): with pytest.raises( - TypeError, - match=r"pytest.approx\(\) does not support nested {}:".format(name), + TypeError, match=fr"pytest.approx\(\) does not support nested {name}:", ): approx(x) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index a85ebdf8e3d..8ca7c78a03a 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1993,7 +1993,7 @@ def test_2(self): pass """ ) - confcut = "--confcutdir={}".format(testdir.tmpdir) + confcut = f"--confcutdir={testdir.tmpdir}" reprec = testdir.inline_run("-v", "-s", confcut) reprec.assertoutcome(passed=8) config = reprec.getcalls("pytest_unconfigure")[0].config @@ -3796,7 +3796,7 @@ def test_foo(request): " test_foos.py::test_foo", "", "Requested fixture 'fix_with_param' defined in:", - "{}:4".format(fixfile), + f"{fixfile}:4", "Requested here:", "test_foos.py:4", "*1 failed*", @@ -3813,9 +3813,9 @@ def test_foo(request): " test_foos.py::test_foo", "", "Requested fixture 'fix_with_param' defined in:", - "{}:4".format(fixfile), + f"{fixfile}:4", "Requested here:", - "{}:4".format(testfile), + f"{testfile}:4", "*1 failed*", ] ) diff --git a/testing/python/raises.py b/testing/python/raises.py index 26931a37844..c3580afad45 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -211,7 +211,7 @@ def test_raises_match(self) -> None: pytest.raises(TypeError, int, match="invalid") def tfunc(match): - raise ValueError("match={}".format(match)) + raise ValueError(f"match={match}") pytest.raises(ValueError, tfunc, match="asdf").match("match=asdf") pytest.raises(ValueError, tfunc, match="").match("match=") diff --git a/testing/test_argcomplete.py b/testing/test_argcomplete.py index 9cab242c0e0..a3224be5126 100644 --- a/testing/test_argcomplete.py +++ b/testing/test_argcomplete.py @@ -11,7 +11,7 @@ def equal_with_bash(prefix, ffc, fc, out=None): res_bash = set(fc(prefix)) retval = set(res) == res_bash if out: - out.write("equal_with_bash({}) {} {}\n".format(prefix, retval, res)) + out.write(f"equal_with_bash({prefix}) {retval} {res}\n") if not retval: out.write(" python - bash: %s\n" % (set(res) - res_bash)) out.write(" bash - python: %s\n" % (res_bash - set(res))) @@ -45,26 +45,16 @@ def __call__(self, prefix, **kwargs): completion = [] if self.allowednames: if self.directories: - files = _wrapcall( - ["bash", "-c", "compgen -A directory -- '{p}'".format(p=prefix)] - ) + files = _wrapcall(["bash", "-c", f"compgen -A directory -- '{prefix}'"]) completion += [f + "/" for f in files] for x in self.allowednames: completion += _wrapcall( - [ - "bash", - "-c", - "compgen -A file -X '!*.{0}' -- '{p}'".format(x, p=prefix), - ] + ["bash", "-c", f"compgen -A file -X '!*.{x}' -- '{prefix}'"] ) else: - completion += _wrapcall( - ["bash", "-c", "compgen -A file -- '{p}'".format(p=prefix)] - ) + completion += _wrapcall(["bash", "-c", f"compgen -A file -- '{prefix}'"]) - anticomp = _wrapcall( - ["bash", "-c", "compgen -A directory -- '{p}'".format(p=prefix)] - ) + anticomp = _wrapcall(["bash", "-c", f"compgen -A directory -- '{prefix}'"]) completion = list(set(completion) - set(anticomp)) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index d6dc9fc986e..f91bc3cb411 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1494,7 +1494,7 @@ def test_tuple(): """ ) result = testdir.runpytest() - result.stdout.fnmatch_lines(["*test_assert_tuple_warning.py:2:*{}*".format(msg)]) + result.stdout.fnmatch_lines([f"*test_assert_tuple_warning.py:2:*{msg}*"]) # tuples with size != 2 should not trigger the warning testdir.makepyfile( diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 98dea5d8854..7dcaf10ea1a 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -198,14 +198,14 @@ class X: lines = msg.splitlines() if verbose > 1: assert lines == [ - "assert {!r} == 42".format(X), - " +{!r}".format(X), + f"assert {X!r} == 42", + f" +{X!r}", " -42", ] elif verbose > 0: assert lines == [ "assert .X'> == 42", - " +{!r}".format(X), + f" +{X!r}", " -42", ] else: @@ -652,7 +652,7 @@ def f1() -> None: assert getmsg(f1) == "assert 42" def my_reprcompare2(op, left, right) -> str: - return "{} {} {}".format(left, op, right) + return f"{left} {op} {right}" monkeypatch.setattr(util, "_reprcompare", my_reprcompare2) @@ -834,9 +834,7 @@ def test_foo(): ) result = testdir.runpytest_subprocess() assert result.ret == 0 - found_names = glob.glob( - "__pycache__/*-pytest-{}.pyc".format(pytest.__version__) - ) + found_names = glob.glob(f"__pycache__/*-pytest-{pytest.__version__}.pyc") assert found_names, "pyc with expected tag not found in names: {}".format( glob.glob("__pycache__/*.pyc") ) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index a911257ce24..6203ebec73b 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -189,9 +189,7 @@ def test_cache_reportheader_external_abspath(testdir, tmpdir_factory): ) ) result = testdir.runpytest("-v") - result.stdout.fnmatch_lines( - ["cachedir: {abscache}".format(abscache=external_cache)] - ) + result.stdout.fnmatch_lines([f"cachedir: {external_cache}"]) def test_cache_show(testdir): diff --git a/testing/test_capture.py b/testing/test_capture.py index 317a5922741..7aeb2d8acd9 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -937,7 +937,7 @@ def lsof_check(): out = subprocess.check_output(("lsof", "-p", str(pid))).decode() except (OSError, subprocess.CalledProcessError, UnicodeDecodeError) as exc: # about UnicodeDecodeError, see note on pytester - pytest.skip("could not run 'lsof' ({!r})".format(exc)) + pytest.skip(f"could not run 'lsof' ({exc!r})") yield out2 = subprocess.check_output(("lsof", "-p", str(pid))).decode() len1 = len([x for x in out.split("\n") if "REG" in x]) diff --git a/testing/test_compat.py b/testing/test_compat.py index 5ddde3250e0..752e2d0e14d 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -35,7 +35,7 @@ def __init__(self): self.left = 1000 def __repr__(self): - return "".format(left=self.left) + return f"" def __getattr__(self, attr): if not self.left: diff --git a/testing/test_config.py b/testing/test_config.py index 7a0c135ef39..39f88d9450d 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1652,7 +1652,7 @@ def pytest_addoption(parser): assert result.ret == ExitCode.USAGE_ERROR result = testdir.runpytest("--version") - result.stderr.fnmatch_lines(["pytest {}".format(pytest.__version__)]) + result.stderr.fnmatch_lines([f"pytest {pytest.__version__}"]) assert result.ret == ExitCode.USAGE_ERROR @@ -1797,12 +1797,7 @@ def test_func(): res = testdir.runpytest() assert res.ret == 2 msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" - res.stdout.fnmatch_lines( - [ - "*{msg}*".format(msg=msg), - "*subdirectory{sep}conftest.py*".format(sep=os.sep), - ] - ) + res.stdout.fnmatch_lines([f"*{msg}*", f"*subdirectory{os.sep}conftest.py*"]) @pytest.mark.parametrize("use_pyargs", [True, False]) def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( @@ -1830,7 +1825,7 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( if use_pyargs: assert msg not in res.stdout.str() else: - res.stdout.fnmatch_lines(["*{msg}*".format(msg=msg)]) + res.stdout.fnmatch_lines([f"*{msg}*"]) def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest( self, testdir @@ -1854,12 +1849,7 @@ def test_func(): res = testdir.runpytest_subprocess() assert res.ret == 2 msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" - res.stdout.fnmatch_lines( - [ - "*{msg}*".format(msg=msg), - "*subdirectory{sep}conftest.py*".format(sep=os.sep), - ] - ) + res.stdout.fnmatch_lines([f"*{msg}*", f"*subdirectory{os.sep}conftest.py*"]) def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives( self, testdir @@ -1887,7 +1877,7 @@ def test_conftest_import_error_repr(tmpdir): path = tmpdir.join("foo/conftest.py") with pytest.raises( ConftestImportFailure, - match=re.escape("RuntimeError: some error (from {})".format(path)), + match=re.escape(f"RuntimeError: some error (from {path})"), ): try: raise RuntimeError("some error") diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 948b621f7c7..f22d5d724f2 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -251,9 +251,7 @@ def test_1(): assert False """ ) - child = testdir.spawn_pytest( - "--show-capture={} --pdb {}".format(showcapture, p1) - ) + child = testdir.spawn_pytest(f"--show-capture={showcapture} --pdb {p1}") if showcapture in ("all", "log"): child.expect("captured log") child.expect("get rekt") @@ -706,7 +704,7 @@ def do_continue(self, arg): set_trace() """ ) - child = testdir.spawn_pytest("--tb=short {} {}".format(p1, capture_arg)) + child = testdir.spawn_pytest(f"--tb=short {p1} {capture_arg}") child.expect("=== SET_TRACE ===") before = child.before.decode("utf8") if not capture_arg: @@ -744,7 +742,7 @@ def test_pdb_used_outside_test(self, testdir): x = 5 """ ) - child = testdir.spawn("{} {}".format(sys.executable, p1)) + child = testdir.spawn(f"{sys.executable} {p1}") child.expect("x = 5") child.expect("Pdb") child.sendeof() @@ -1085,12 +1083,12 @@ def test_func_kw(myparam, request, func="func_kw"): child.expect_exact(func) child.expect_exact("Pdb") child.sendline("args") - child.expect_exact("{} = 1\r\n".format(argname)) + child.expect_exact(f"{argname} = 1\r\n") child.expect_exact("Pdb") child.sendline("c") child.expect_exact("Pdb") child.sendline("args") - child.expect_exact("{} = 2\r\n".format(argname)) + child.expect_exact(f"{argname} = 2\r\n") child.expect_exact("Pdb") child.sendline("c") child.expect_exact("> PDB continue (IO-capturing resumed) >") diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 13b85979782..0e8bba980f0 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1494,7 +1494,7 @@ def test_is_setup_py_not_named_setup_py(tmpdir): @pytest.mark.parametrize("mod", ("setuptools", "distutils.core")) def test_is_setup_py_is_a_setup_py(tmpdir, mod): setup_py = tmpdir.join("setup.py") - setup_py.write('from {} import setup; setup(name="foo")'.format(mod)) + setup_py.write(f'from {mod} import setup; setup(name="foo")') assert _is_setup_py(setup_py) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index 87a195bf807..4d0b32eded7 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -124,7 +124,7 @@ def test(): "-mpytest", testdir.tmpdir, "-o", - "faulthandler_timeout={}".format(faulthandler_timeout), + f"faulthandler_timeout={faulthandler_timeout}", ) # ensure warning is emitted if faulthandler_timeout is configured warning_line = "*faulthandler.py*faulthandler module enabled before*" diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index 6116242ec0d..6f6d533372e 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -6,9 +6,7 @@ def test_version_verbose(testdir, pytestconfig): testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") result = testdir.runpytest("--version", "--version") assert result.ret == 0 - result.stderr.fnmatch_lines( - ["*pytest*{}*imported from*".format(pytest.__version__)] - ) + result.stderr.fnmatch_lines([f"*pytest*{pytest.__version__}*imported from*"]) if pytestconfig.pluginmanager.list_plugin_distinfo(): result.stderr.fnmatch_lines(["*setuptools registered plugins:", "*at*"]) @@ -18,7 +16,7 @@ def test_version_less_verbose(testdir, pytestconfig): result = testdir.runpytest("--version") assert result.ret == 0 # p = py.path.local(py.__file__).dirpath() - result.stderr.fnmatch_lines(["pytest {}".format(pytest.__version__)]) + result.stderr.fnmatch_lines([f"pytest {pytest.__version__}"]) def test_help(testdir): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 23ef3f34773..ef4ff6a7750 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -245,9 +245,7 @@ def test_foo(): pass """ ) - result, dom = run_and_parse( - "-o", "junit_duration_report={}".format(duration_report) - ) + result, dom = run_and_parse("-o", f"junit_duration_report={duration_report}") node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") val = float(tnode["time"]) diff --git a/testing/test_link_resolve.py b/testing/test_link_resolve.py index f43f7ded567..7eaf4124796 100644 --- a/testing/test_link_resolve.py +++ b/testing/test_link_resolve.py @@ -77,7 +77,5 @@ def test_foo(): # i.e.: Expect drive on windows because we just have drive:filename, whereas # we expect a relative path on Linux. - expect = ( - "*{}*".format(subst_p) if sys.platform == "win32" else "*sub2/test_foo.py*" - ) + expect = f"*{subst_p}*" if sys.platform == "win32" else "*sub2/test_foo.py*" result.stdout.fnmatch_lines([expect]) diff --git a/testing/test_main.py b/testing/test_main.py index 8ec7b8111cd..3e94668e82f 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -48,20 +48,20 @@ def pytest_internalerror(excrepr, excinfo): if exc == SystemExit: assert result.stdout.lines[-3:] == [ - 'INTERNALERROR> File "{}", line 4, in pytest_sessionstart'.format(c1), + f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart', 'INTERNALERROR> raise SystemExit("boom")', "INTERNALERROR> SystemExit: boom", ] else: assert result.stdout.lines[-3:] == [ - 'INTERNALERROR> File "{}", line 4, in pytest_sessionstart'.format(c1), + f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart', 'INTERNALERROR> raise ValueError("boom")', "INTERNALERROR> ValueError: boom", ] if returncode is False: assert result.stderr.lines == ["mainloop: caught unexpected SystemExit!"] else: - assert result.stderr.lines == ["Exit: exiting after {}...".format(exc.__name__)] + assert result.stderr.lines == [f"Exit: exiting after {exc.__name__}..."] @pytest.mark.parametrize("returncode", (None, 42)) diff --git a/testing/test_mark.py b/testing/test_mark.py index 5d5e0cf42f7..b3240b1b11f 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1094,7 +1094,7 @@ def test_foo(): pass """ ) - expected = "ERROR: Wrong expression passed to '-m': {}: *".format(expr) + expected = f"ERROR: Wrong expression passed to '-m': {expr}: *" result = testdir.runpytest(foo, "-m", expr) result.stderr.fnmatch_lines([expected]) assert result.ret == ExitCode.USAGE_ERROR diff --git a/testing/test_meta.py b/testing/test_meta.py index 97b2e1a1a49..25a46a35f05 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -27,6 +27,6 @@ def test_no_warnings(module: str) -> None: subprocess.check_call(( sys.executable, "-W", "error", - "-c", "__import__({!r})".format(module), + "-c", f"__import__({module!r})", )) # fmt: on diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 46fab0ce893..d27000f3b17 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -580,20 +580,20 @@ def test_linematcher_no_matching(function) -> None: obtained = str(e.value).splitlines() if function == "no_fnmatch_line": assert obtained == [ - "nomatch: '{}'".format(good_pattern), + f"nomatch: '{good_pattern}'", " and: 'cachedir: .pytest_cache'", " and: 'collecting ... collected 1 item'", " and: ''", - "fnmatch: '{}'".format(good_pattern), + f"fnmatch: '{good_pattern}'", " with: 'show_fixtures_per_test.py OK'", ] else: assert obtained == [ - " nomatch: '{}'".format(good_pattern), + f" nomatch: '{good_pattern}'", " and: 'cachedir: .pytest_cache'", " and: 'collecting ... collected 1 item'", " and: ''", - "re.match: '{}'".format(good_pattern), + f"re.match: '{good_pattern}'", " with: 'show_fixtures_per_test.py OK'", ] diff --git a/testing/test_runner.py b/testing/test_runner.py index d3b7729f728..0101f68233b 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -472,7 +472,7 @@ def test_callinfo() -> None: ci2 = runner.CallInfo.from_call(lambda: 0 / 0, "collect") assert ci2.when == "collect" assert not hasattr(ci2, "result") - assert repr(ci2) == "".format(ci2.excinfo) + assert repr(ci2) == f"" assert str(ci2) == repr(ci2) assert ci2.excinfo @@ -481,7 +481,7 @@ def raise_assertion(): assert 0, "assert_msg" ci3 = runner.CallInfo.from_call(raise_assertion, "call") - assert repr(ci3) == "".format(ci3.excinfo) + assert repr(ci3) == f"" assert "\n" not in repr(ci3) diff --git a/testing/test_session.py b/testing/test_session.py index 1800771dad5..446d764c395 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -348,10 +348,10 @@ def test_one(): """ ) - result = testdir.runpytest("--rootdir={}".format(path)) + result = testdir.runpytest(f"--rootdir={path}") result.stdout.fnmatch_lines( [ - "*rootdir: {}/root".format(testdir.tmpdir), + f"*rootdir: {testdir.tmpdir}/root", "root/test_rootdir_option_arg.py *", "*1 passed*", ] diff --git a/testing/test_skipping.py b/testing/test_skipping.py index b32d2267d21..bf014e343c0 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -758,10 +758,7 @@ def test_foobar(): ) result = testdir.runpytest("-s", "-rsxX") result.stdout.fnmatch_lines( - [ - "*{msg1}*test_foo.py*second_condition*".format(msg1=msg1), - "*1 {msg2}*".format(msg2=msg2), - ] + [f"*{msg1}*test_foo.py*second_condition*", f"*1 {msg2}*"] ) assert result.ret == 0 diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 1ff308fa603..490f2df09f9 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1725,9 +1725,9 @@ class fake_session: tr._main_color = None print("Based on stats: %s" % stats_arg) - print('Expect summary: "{}"; with color "{}"'.format(exp_line, exp_color)) + print(f'Expect summary: "{exp_line}"; with color "{exp_color}"') (line, color) = tr.build_summary_stats_line() - print('Actually got: "{}"; with color "{}"'.format(line, color)) + print(f'Actually got: "{line}"; with color "{color}"') assert line == exp_line assert color == exp_color @@ -1773,7 +1773,7 @@ def test_normal_verbosity(self, testdir, test_files): [ "test_one.py .", "test_two.py F", - "sub{}test_three.py .F.".format(os.sep), + f"sub{os.sep}test_three.py .F.", "*2 failed, 3 passed in*", ] ) @@ -1784,9 +1784,9 @@ def test_verbose(self, testdir, test_files): [ "test_one.py::test_one PASSED", "test_two.py::test_two FAILED", - "sub{}test_three.py::test_three_1 PASSED".format(os.sep), - "sub{}test_three.py::test_three_2 FAILED".format(os.sep), - "sub{}test_three.py::test_three_3 PASSED".format(os.sep), + f"sub{os.sep}test_three.py::test_three_1 PASSED", + f"sub{os.sep}test_three.py::test_three_2 FAILED", + f"sub{os.sep}test_three.py::test_three_3 PASSED", "*2 failed, 3 passed in*", ] ) @@ -2146,7 +2146,7 @@ def check(msg, width, expected): actual = _get_line_with_reprcrash_message(config, rep(), width) # type: ignore assert actual == expected - if actual != "{} {}".format(mocked_verbose_word, mocked_pos): + if actual != f"{mocked_verbose_word} {mocked_pos}": assert len(actual) <= width assert wcswidth(actual) <= width diff --git a/testing/test_unittest.py b/testing/test_unittest.py index f3f4d4e06ac..1aa885264af 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1086,9 +1086,9 @@ def test_error_message_with_parametrized_fixtures(testdir): ) def test_setup_inheritance_skipping(testdir, test_name, expected_outcome): """Issue #4700""" - testdir.copy_example("unittest/{}".format(test_name)) + testdir.copy_example(f"unittest/{test_name}") result = testdir.runpytest() - result.stdout.fnmatch_lines(["* {} in *".format(expected_outcome)]) + result.stdout.fnmatch_lines([f"* {expected_outcome} in *"]) def test_BdbQuit(testdir): diff --git a/testing/test_warnings.py b/testing/test_warnings.py index b7a231094fd..3af253ecda2 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -712,7 +712,7 @@ def test_issue4445_preparse(self, testdir, capwarn): file, _, func = location assert "could not load initial conftests" in str(warning.message) - assert "config{sep}__init__.py".format(sep=os.sep) in file + assert f"config{os.sep}__init__.py" in file assert func == "_preparse" @pytest.mark.filterwarnings("default") @@ -748,7 +748,7 @@ def test_issue4445_import_plugin(self, testdir, capwarn): file, _, func = location assert "skipped plugin 'some_plugin': thing" in str(warning.message) - assert "config{sep}__init__.py".format(sep=os.sep) in file + assert f"config{os.sep}__init__.py" in file assert func == "_warn_about_skipped_plugins" def test_issue4445_issue5928_mark_generator(self, testdir): From bf09e7792f812476a1b85db7616ae98fd276f66e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 3 Oct 2020 14:21:41 +0300 Subject: [PATCH 674/823] fixtures: some type annotations --- src/_pytest/fixtures.py | 69 +++++++++++++++++++++----------------- testing/python/fixtures.py | 2 +- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 34d038e9e1c..e6a1d8fdc8c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -35,6 +35,7 @@ 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 @@ -51,6 +52,7 @@ from _pytest.deprecated import FILLFUNCARGS from _pytest.mark import Mark from _pytest.mark import ParameterSet +from _pytest.mark.structures import MarkDecorator from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME from _pytest.pathlib import absolutepath @@ -103,24 +105,9 @@ class PseudoFixtureDef(Generic[_FixtureValue]): def pytest_sessionstart(session: "Session") -> None: - import _pytest.python - import _pytest.nodes - - scopename2class.update( - { - "package": _pytest.python.Package, - "class": _pytest.python.Class, - "module": _pytest.python.Module, - "function": _pytest.nodes.Item, - "session": _pytest.main.Session, - } - ) session._fixturemanager = FixtureManager(session) -scopename2class = {} # type: Dict[str, Type[nodes.Node]] - - def get_scope_package(node, fixturedef: "FixtureDef[object]"): import pytest @@ -136,11 +123,24 @@ def get_scope_package(node, fixturedef: "FixtureDef[object]"): return current -def get_scope_node(node, scope): - cls = scopename2class.get(scope) - if cls is None: - raise ValueError("unknown scope") - return node.getparent(cls) +def get_scope_node( + node: "nodes.Node", scope: "_Scope" +) -> Optional[Union["nodes.Item", "nodes.Collector"]]: + import _pytest.python + import _pytest.nodes + + if scope == "function": + return node.getparent(_pytest.nodes.Item) + elif scope == "class": + return node.getparent(_pytest.python.Class) + elif scope == "module": + return node.getparent(_pytest.python.Module) + elif scope == "package": + return node.getparent(_pytest.python.Package) + elif scope == "session": + return node.getparent(_pytest.main.Session) + else: + assert_never(scope) def add_funcarg_pseudo_fixture_def( @@ -519,9 +519,9 @@ def keywords(self): return self.node.keywords @property - def session(self): + def session(self) -> "Session": """Pytest session object.""" - return self._pyfuncitem.session + return self._pyfuncitem.session # type: ignore[no-any-return] def addfinalizer(self, finalizer: Callable[[], object]) -> None: """Add finalizer/teardown function to be called after the last test @@ -535,7 +535,7 @@ def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None: finalizer=finalizer, colitem=colitem ) - def applymarker(self, marker) -> None: + def applymarker(self, marker: Union[str, MarkDecorator]) -> None: """Apply a marker to a single test function invocation. This method is useful if you don't want to have a keyword/marker @@ -685,7 +685,9 @@ def _schedule_finalizers( functools.partial(fixturedef.finish, request=subrequest), subrequest.node ) - def _check_scope(self, argname, invoking_scope: "_Scope", requested_scope) -> None: + def _check_scope( + self, argname: str, invoking_scope: "_Scope", requested_scope: "_Scope", + ) -> None: if argname == "request": return if scopemismatch(invoking_scope, requested_scope): @@ -709,11 +711,11 @@ def _factorytraceback(self) -> List[str]: lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) return lines - def _getscopeitem(self, scope): + def _getscopeitem(self, scope: "_Scope") -> Union["nodes.Item", "nodes.Collector"]: if scope == "function": # This might also be a non-function Item despite its attribute name. - return self._pyfuncitem - if scope == "package": + node: Optional[Union["nodes.Item", "nodes.Collector"]] = self._pyfuncitem + elif scope == "package": # FIXME: _fixturedef is not defined on FixtureRequest (this class), # but on FixtureRequest (a subclass). node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined] @@ -962,7 +964,7 @@ class FixtureDef(Generic[_FixtureValue]): def __init__( self, fixturemanager: "FixtureManager", - baseid, + baseid: Optional[str], argname: str, func: "_FixtureFunc[_FixtureValue]", scope: "Union[_Scope, Callable[[str, Config], _Scope]]", @@ -1144,7 +1146,9 @@ def _params_converter( return tuple(params) if params is not None else None -def wrap_function_to_error_out_if_called_directly(function, fixture_marker): +def wrap_function_to_error_out_if_called_directly( + function: _FixtureFunction, fixture_marker: "FixtureFunctionMarker", +) -> _FixtureFunction: """Wrap the given fixture function so we can raise an error about it being called directly, instead of used as an argument in a test function.""" message = ( @@ -1162,7 +1166,7 @@ def result(*args, **kwargs): # further than this point and lose useful wrappings like @mock.patch (#3774). result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined] - return result + return cast(_FixtureFunction, result) @final @@ -1485,7 +1489,10 @@ def _getautousenames(self, nodeid: str) -> List[str]: return autousenames def getfixtureclosure( - self, fixturenames: Tuple[str, ...], parentnode, ignore_args: Sequence[str] = () + self, + fixturenames: Tuple[str, ...], + parentnode: "nodes.Node", + ignore_args: Sequence[str] = (), ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]: # Collect the closure of all fixtures, starting with the given # fixturenames as the initial set. As we have to visit all diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index a85ebdf8e3d..07e1c6f731b 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1059,7 +1059,7 @@ def test_func2(self, something): req1.applymarker(pytest.mark.skipif) assert "skipif" in item1.keywords with pytest.raises(ValueError): - req1.applymarker(42) + req1.applymarker(42) # type: ignore[arg-type] def test_accesskeywords(self, testdir): testdir.makepyfile( From d0a3f1dcbce82dfa5ea681a3937153d6592657a1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 3 Oct 2020 14:56:19 +0300 Subject: [PATCH 675/823] nodes: remove cyclic dependency on _pytest.fixtures - Change the fixtures plugin to store its one piece of data on the node's Store instead of directly. - Import FixtureLookupError lazily. --- src/_pytest/fixtures.py | 67 +++++++++++++++++++++++------------------ src/_pytest/nodes.py | 9 ++---- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index e6a1d8fdc8c..8aa03d12132 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -29,6 +29,7 @@ import py import _pytest +from _pytest import nodes from _pytest._code import getfslineno from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr @@ -56,13 +57,13 @@ from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME from _pytest.pathlib import absolutepath +from _pytest.store import StoreKey if TYPE_CHECKING: from typing import Deque from typing import NoReturn from typing_extensions import Literal - from _pytest import nodes from _pytest.main import Session from _pytest.python import CallSpec2 from _pytest.python import Function @@ -124,13 +125,12 @@ def get_scope_package(node, fixturedef: "FixtureDef[object]"): def get_scope_node( - node: "nodes.Node", scope: "_Scope" -) -> Optional[Union["nodes.Item", "nodes.Collector"]]: + node: nodes.Node, scope: "_Scope" +) -> Optional[Union[nodes.Item, nodes.Collector]]: import _pytest.python - import _pytest.nodes if scope == "function": - return node.getparent(_pytest.nodes.Item) + return node.getparent(nodes.Item) elif scope == "class": return node.getparent(_pytest.python.Class) elif scope == "module": @@ -143,8 +143,12 @@ def get_scope_node( assert_never(scope) +# Used for storing artificial fixturedefs for direct parametrization. +name2pseudofixturedef_key = StoreKey[Dict[str, "FixtureDef[Any]"]]() + + def add_funcarg_pseudo_fixture_def( - collector, metafunc: "Metafunc", fixturemanager: "FixtureManager" + collector: nodes.Collector, metafunc: "Metafunc", fixturemanager: "FixtureManager" ) -> None: # This function will transform all collected calls to functions # if they use direct funcargs (i.e. direct parametrization) @@ -186,8 +190,15 @@ def add_funcarg_pseudo_fixture_def( assert scope == "class" and isinstance(collector, _pytest.python.Module) # Use module-level collector for class-scope (for now). node = collector - if node and argname in node._name2pseudofixturedef: - arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]] + if node is None: + name2pseudofixturedef = None + else: + default: Dict[str, FixtureDef[Any]] = {} + name2pseudofixturedef = node._store.setdefault( + name2pseudofixturedef_key, default + ) + if name2pseudofixturedef is not None and argname in name2pseudofixturedef: + arg2fixturedefs[argname] = [name2pseudofixturedef[argname]] else: fixturedef = FixtureDef( fixturemanager=fixturemanager, @@ -200,8 +211,8 @@ def add_funcarg_pseudo_fixture_def( ids=None, ) arg2fixturedefs[argname] = [fixturedef] - if node is not None: - node._name2pseudofixturedef[argname] = fixturedef + if name2pseudofixturedef is not None: + name2pseudofixturedef[argname] = fixturedef def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: @@ -222,7 +233,7 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: _Key = Tuple[object, ...] -def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator[_Key]: +def get_parametrized_fixture_keys(item: nodes.Item, scopenum: int) -> Iterator[_Key]: """Return list of keys for all parametrized arguments which match the specified scope. """ assert scopenum < scopenum_function # function @@ -256,7 +267,7 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator # setups and teardowns. -def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]": +def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]: argkeys_cache = {} # type: Dict[int, Dict[nodes.Item, Dict[_Key, None]]] items_by_argkey = {} # type: Dict[int, Dict[_Key, Deque[nodes.Item]]] for scopenum in range(0, scopenum_function): @@ -278,15 +289,15 @@ def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]": item_d[key].append(item) # cast is a workaround for https://github.com/python/typeshed/issues/3800. items_dict = cast( - "Dict[nodes.Item, None]", order_preserving_dict.fromkeys(items, None) + Dict[nodes.Item, None], order_preserving_dict.fromkeys(items, None) ) return list(reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, 0)) def fix_cache_order( - item: "nodes.Item", - argkeys_cache: "Dict[int, Dict[nodes.Item, Dict[_Key, None]]]", - items_by_argkey: "Dict[int, Dict[_Key, Deque[nodes.Item]]]", + item: nodes.Item, + argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]], + items_by_argkey: Dict[int, Dict[_Key, "Deque[nodes.Item]"]], ) -> None: for scopenum in range(0, scopenum_function): for key in argkeys_cache[scopenum].get(item, []): @@ -294,11 +305,11 @@ def fix_cache_order( def reorder_items_atscope( - items: "Dict[nodes.Item, None]", - argkeys_cache: "Dict[int, Dict[nodes.Item, Dict[_Key, None]]]", - items_by_argkey: "Dict[int, Dict[_Key, Deque[nodes.Item]]]", + items: Dict[nodes.Item, None], + argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]], + items_by_argkey: Dict[int, Dict[_Key, "Deque[nodes.Item]"]], scopenum: int, -) -> "Dict[nodes.Item, None]": +) -> Dict[nodes.Item, None]: if scopenum >= scopenum_function or len(items) < 3: return items ignore = set() # type: Set[Optional[_Key]] @@ -711,10 +722,10 @@ def _factorytraceback(self) -> List[str]: lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) return lines - def _getscopeitem(self, scope: "_Scope") -> Union["nodes.Item", "nodes.Collector"]: + def _getscopeitem(self, scope: "_Scope") -> Union[nodes.Item, nodes.Collector]: if scope == "function": # This might also be a non-function Item despite its attribute name. - node: Optional[Union["nodes.Item", "nodes.Collector"]] = self._pyfuncitem + node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem elif scope == "package": # FIXME: _fixturedef is not defined on FixtureRequest (this class), # but on FixtureRequest (a subclass). @@ -1414,7 +1425,7 @@ def __init__(self, session: "Session") -> None: ] # type: List[Tuple[str, List[str]]] session.config.pluginmanager.register(self, "funcmanage") - def _get_direct_parametrize_args(self, node: "nodes.Node") -> List[str]: + def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]: """Return all direct parametrization arguments of a node, so we don't mistake them for fixtures. @@ -1434,7 +1445,7 @@ def _get_direct_parametrize_args(self, node: "nodes.Node") -> List[str]: return parametrize_argnames def getfixtureinfo( - self, node: "nodes.Node", func, cls, funcargs: bool = True + self, node: nodes.Node, func, cls, funcargs: bool = True ) -> FuncFixtureInfo: if funcargs and not getattr(node, "nofuncargs", False): argnames = getfuncargnames(func, name=node.name, cls=cls) @@ -1458,8 +1469,6 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: except AttributeError: pass else: - from _pytest import nodes - # Construct the base nodeid which is later used to check # what fixtures are visible for particular tests (as denoted # by their test id). @@ -1491,7 +1500,7 @@ def _getautousenames(self, nodeid: str) -> List[str]: def getfixtureclosure( self, fixturenames: Tuple[str, ...], - parentnode: "nodes.Node", + parentnode: nodes.Node, ignore_args: Sequence[str] = (), ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]: # Collect the closure of all fixtures, starting with the given @@ -1586,7 +1595,7 @@ def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]: # Try next super fixture, if any. - def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None: + def pytest_collection_modifyitems(self, items: List[nodes.Item]) -> None: # Separate parametrized setups. items[:] = reorder_items(items) @@ -1667,8 +1676,6 @@ def getfixturedefs( def _matchfactories( self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str ) -> Iterator[FixtureDef[Any]]: - from _pytest import nodes - for fixturedef in fixturedefs: if nodes.ischildnode(fixturedef.baseid, nodeid): yield fixturedef diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 1489d097718..96bac46af5c 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -2,9 +2,7 @@ import warnings from functools import lru_cache from pathlib import Path -from typing import Any from typing import Callable -from typing import Dict from typing import Iterable from typing import Iterator from typing import List @@ -27,8 +25,6 @@ from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH -from _pytest.fixtures import FixtureDef -from _pytest.fixtures import FixtureLookupError from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords @@ -170,9 +166,6 @@ def __init__( #: Allow adding of extra keywords to use for matching. self.extra_keyword_matches = set() # type: Set[str] - # Used for storing artificial fixturedefs for direct parametrization. - self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef[Any]] - if nodeid is not None: assert "::()" not in nodeid self._nodeid = nodeid @@ -366,6 +359,8 @@ def _repr_failure_py( excinfo: ExceptionInfo[BaseException], style: "Optional[_TracebackStyle]" = None, ) -> TerminalRepr: + from _pytest.fixtures import FixtureLookupError + if isinstance(excinfo.value, ConftestImportFailure): excinfo = ExceptionInfo(excinfo.value.excinfo) if isinstance(excinfo.value, fail.Exception): From 8593b57666ff8412b0f56f60faca80df94e4ef96 Mon Sep 17 00:00:00 2001 From: Albert Villanova del Moral <8515462+albertvillanova@users.noreply.github.com> Date: Sun, 4 Oct 2020 08:54:43 +0200 Subject: [PATCH 676/823] Update link to numpy --- 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 e00a1b25da7..71ba6d81577 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -456,7 +456,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: think of ``b`` as the reference value. Support for comparing sequences is provided by ``numpy.allclose``. `More information...`__ - __ http://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.isclose.html + __ https://numpy.org/doc/stable/reference/generated/numpy.isclose.html - ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b`` are within an absolute tolerance of ``1e-7``. No relative tolerance is From 703e89134c2b0c21225014e3a89c17ab062c0ab9 Mon Sep 17 00:00:00 2001 From: William Jamir Silva Date: Mon, 5 Oct 2020 14:04:37 -0300 Subject: [PATCH 677/823] Update reference.rst informing the default junit_family (#7860) Co-authored-by: Bruno Oliveira --- doc/en/reference.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index d1540a8ff2a..15d8250844f 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1236,12 +1236,13 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: junit_family .. versionadded:: 4.2 + .. versionchanged:: 6.1 + Default changed to ``xunit2``. Configures the format of the generated JUnit XML file. The possible options are: - * ``xunit1`` (or ``legacy``): produces old style output, compatible with the xunit 1.0 format. **This is the default**. - * ``xunit2``: produces `xunit 2.0 style output `__, - which should be more compatible with latest Jenkins versions. + * ``xunit1`` (or ``legacy``): produces old style output, compatible with the xunit 1.0 format. + * ``xunit2``: produces `xunit 2.0 style output `__, which should be more compatible with latest Jenkins versions. **This is the default**. .. code-block:: ini From 33d119f71a60d1b41686c04a52c3a570fdcd506c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 5 Oct 2020 18:13:05 -0700 Subject: [PATCH 678/823] py36+: com2ann --- .pre-commit-config.yaml | 4 + src/_pytest/_argcomplete.py | 2 +- src/_pytest/_code/code.py | 46 +++++------ src/_pytest/_code/source.py | 6 +- src/_pytest/_io/terminalwriter.py | 6 +- src/_pytest/assertion/__init__.py | 2 +- src/_pytest/assertion/rewrite.py | 40 +++++----- src/_pytest/assertion/util.py | 12 +-- src/_pytest/cacheprovider.py | 14 ++-- src/_pytest/capture.py | 14 ++-- src/_pytest/compat.py | 2 +- src/_pytest/config/__init__.py | 44 +++++------ src/_pytest/config/argparsing.py | 26 +++---- src/_pytest/config/findpaths.py | 4 +- src/_pytest/debugging.py | 15 ++-- src/_pytest/doctest.py | 24 +++--- src/_pytest/fixtures.py | 78 +++++++++---------- src/_pytest/helpconfig.py | 2 +- src/_pytest/junitxml.py | 36 ++++----- src/_pytest/logging.py | 28 +++---- src/_pytest/main.py | 52 ++++++------- src/_pytest/mark/expression.py | 10 +-- src/_pytest/mark/structures.py | 8 +- src/_pytest/monkeypatch.py | 12 ++- src/_pytest/nodes.py | 20 +++-- src/_pytest/outcomes.py | 4 +- src/_pytest/pastebin.py | 4 +- src/_pytest/pytester.py | 20 ++--- src/_pytest/python.py | 60 +++++++------- src/_pytest/python_api.py | 16 ++-- src/_pytest/recwarn.py | 2 +- src/_pytest/reports.py | 42 +++++----- src/_pytest/runner.py | 18 ++--- src/_pytest/stepwise.py | 2 +- src/_pytest/store.py | 2 +- src/_pytest/terminal.py | 74 +++++++++--------- src/_pytest/unittest.py | 10 +-- testing/code/test_excinfo.py | 6 +- testing/code/test_source.py | 2 +- testing/example_scripts/issue_519.py | 2 +- .../unittest/test_unittest_asyncio.py | 2 +- .../unittest/test_unittest_asynctest.py | 2 +- testing/logging/test_formatter.py | 4 +- testing/python/collect.py | 2 +- testing/python/integration.py | 4 +- testing/python/metafunc.py | 20 ++--- testing/test_assertion.py | 2 +- testing/test_assertrewrite.py | 6 +- testing/test_compat.py | 6 +- testing/test_config.py | 4 +- testing/test_junitxml.py | 9 ++- testing/test_meta.py | 2 +- testing/test_monkeypatch.py | 4 +- testing/test_pastebin.py | 2 +- testing/test_pluginmanager.py | 2 +- testing/test_pytester.py | 4 +- testing/test_reports.py | 6 +- testing/test_runner.py | 8 +- testing/test_runner_xunit.py | 2 +- testing/test_tmpdir.py | 2 +- testing/test_unittest.py | 4 +- testing/test_warnings.py | 6 +- 62 files changed, 431 insertions(+), 443 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 26289b72fd3..75941dcd934 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,6 +44,10 @@ repos: - id: setup-cfg-fmt # TODO: when upgrading setup-cfg-fmt this can be removed args: [--max-py-version=3.9] +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.6.0 + hooks: + - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.782 # NOTE: keep this in sync with setup.cfg. hooks: diff --git a/src/_pytest/_argcomplete.py b/src/_pytest/_argcomplete.py index 3dbdf9318be..63deb667d0f 100644 --- a/src/_pytest/_argcomplete.py +++ b/src/_pytest/_argcomplete.py @@ -103,7 +103,7 @@ def __call__(self, prefix: str, **kwargs: Any) -> List[str]: import argcomplete.completers except ImportError: sys.exit(-1) - filescompleter = FastFilesCompleter() # type: Optional[FastFilesCompleter] + filescompleter: Optional[FastFilesCompleter] = FastFilesCompleter() def try_argcomplete(parser: argparse.ArgumentParser) -> None: argcomplete.autocomplete(parser, always_complete_options=False) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 7054ef40724..2371b44d938 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -164,7 +164,7 @@ def getargs(self, var: bool = False): class TracebackEntry: """A single entry in a Traceback.""" - _repr_style = None # type: Optional[Literal["short", "long"]] + _repr_style: Optional['Literal["short", "long"]'] = None exprinfo = None def __init__( @@ -246,9 +246,9 @@ def ishidden(self) -> bool: Mostly for internal use. """ - tbh = ( + tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = ( False - ) # type: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] + ) for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals): # in normal cases, f_locals and f_globals are dictionaries # however via `exec(...)` / `eval(...)` they can be other types @@ -301,7 +301,7 @@ def __init__( if isinstance(tb, TracebackType): def f(cur: TracebackType) -> Iterable[TracebackEntry]: - cur_ = cur # type: Optional[TracebackType] + cur_: Optional[TracebackType] = cur while cur_ is not None: yield TracebackEntry(cur_, excinfo=excinfo) cur_ = cur_.tb_next @@ -381,7 +381,7 @@ def getcrashentry(self) -> TracebackEntry: def recursionindex(self) -> Optional[int]: """Return the index of the frame/TracebackEntry where recursion originates if appropriate, None if no recursion occurred.""" - cache = {} # type: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] + cache: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] = {} for i, entry in enumerate(self): # id for the code.raw is needed to work around # the strange metaprogramming in the decorator lib from pypi @@ -760,7 +760,7 @@ def repr_traceback_entry( entry: TracebackEntry, excinfo: Optional[ExceptionInfo[BaseException]] = None, ) -> "ReprEntry": - lines = [] # type: List[str] + lines: List[str] = [] style = entry._repr_style if entry._repr_style is not None else self.style if style in ("short", "long"): source = self._getentrysource(entry) @@ -842,7 +842,7 @@ def _truncate_recursive_traceback( recursionindex = traceback.recursionindex() except Exception as e: max_frames = 10 - extraline = ( + extraline: Optional[str] = ( "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n" " The following exception happened when comparing locals in the stack frame:\n" " {exc_type}: {exc_msg}\n" @@ -852,7 +852,7 @@ def _truncate_recursive_traceback( exc_msg=str(e), max_frames=max_frames, total=len(traceback), - ) # type: Optional[str] + ) # Type ignored because adding two instaces of a List subtype # currently incorrectly has type List instead of the subtype. traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore @@ -868,20 +868,20 @@ def _truncate_recursive_traceback( def repr_excinfo( self, excinfo: ExceptionInfo[BaseException] ) -> "ExceptionChainRepr": - repr_chain = ( - [] - ) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]] - e = excinfo.value # type: Optional[BaseException] - excinfo_ = excinfo # type: Optional[ExceptionInfo[BaseException]] + repr_chain: List[ + Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]] + ] = [] + e: Optional[BaseException] = excinfo.value + excinfo_: Optional[ExceptionInfo[BaseException]] = excinfo descr = None - seen = set() # type: Set[int] + seen: Set[int] = set() while e is not None and id(e) not in seen: seen.add(id(e)) if excinfo_: reprtraceback = self.repr_traceback(excinfo_) - reprcrash = ( + reprcrash: Optional[ReprFileLocation] = ( excinfo_._getreprcrash() if self.style != "value" else None - ) # type: Optional[ReprFileLocation] + ) else: # Fallback to native repr if the exception doesn't have a traceback: # ExceptionInfo objects require a full traceback to work. @@ -936,11 +936,11 @@ def toterminal(self, tw: TerminalWriter) -> None: @attr.s(eq=False) class ExceptionRepr(TerminalRepr): # Provided by subclasses. - reprcrash = None # type: Optional[ReprFileLocation] - reprtraceback = None # type: ReprTraceback + reprcrash: Optional["ReprFileLocation"] + reprtraceback: "ReprTraceback" def __attrs_post_init__(self) -> None: - self.sections = [] # type: List[Tuple[str, str, str]] + self.sections: List[Tuple[str, str, str]] = [] def addsection(self, name: str, content: str, sep: str = "-") -> None: self.sections.append((name, content, sep)) @@ -1022,7 +1022,7 @@ def __init__(self, tblines: Sequence[str]) -> None: @attr.s(eq=False) class ReprEntryNative(TerminalRepr): lines = attr.ib(type=Sequence[str]) - style = "native" # type: _TracebackStyle + style: "_TracebackStyle" = "native" def toterminal(self, tw: TerminalWriter) -> None: tw.write("".join(self.lines)) @@ -1058,9 +1058,9 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None: # such as "> assert 0" fail_marker = f"{FormattedExcinfo.fail_marker} " indent_size = len(fail_marker) - indents = [] # type: List[str] - source_lines = [] # type: List[str] - failure_lines = [] # type: List[str] + indents: List[str] = [] + source_lines: List[str] = [] + failure_lines: List[str] = [] for index, line in enumerate(self.lines): is_failure_line = line.startswith(fail_marker) if is_failure_line: diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 028ff48a3fe..c63a42360c6 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -21,7 +21,7 @@ class Source: def __init__(self, obj: object = None) -> None: if not obj: - self.lines = [] # type: List[str] + self.lines: List[str] = [] elif isinstance(obj, Source): self.lines = obj.lines elif isinstance(obj, (tuple, list)): @@ -144,12 +144,12 @@ def deindent(lines: Iterable[str]) -> List[str]: def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]: # Flatten all statements and except handlers into one lineno-list. # AST's line numbers start indexing at 1. - values = [] # type: List[int] + values: List[int] = [] for x in ast.walk(node): if isinstance(x, (ast.stmt, ast.ExceptHandler)): values.append(x.lineno - 1) for name in ("finalbody", "orelse"): - val = getattr(x, name, None) # type: Optional[List[ast.stmt]] + val: Optional[List[ast.stmt]] = getattr(x, name, None) if val: # Treat the finally/orelse part as its own statement. values.append(val[0].lineno - 1 - 1) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 9077d41935c..8edf4cd75fa 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -76,7 +76,7 @@ def __init__(self, file: Optional[TextIO] = None) -> None: self._file = file self.hasmarkup = should_do_markup(file) self._current_line = "" - self._terminal_width = None # type: Optional[int] + self._terminal_width: Optional[int] = None self.code_highlight = True @property @@ -204,7 +204,7 @@ def _highlight(self, source: str) -> str: except ImportError: return source else: - highlighted = highlight( + highlighted: str = highlight( source, PythonLexer(), TerminalFormatter(bg="dark") - ) # type: str + ) return highlighted diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 554ac191d6a..a18cf198df0 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -83,7 +83,7 @@ class AssertionState: def __init__(self, config: Config, mode) -> None: self.mode = mode self.trace = config.trace.root.get("assertion") - self.hook = None # type: Optional[rewrite.AssertionRewritingHook] + self.hook: Optional[rewrite.AssertionRewritingHook] = None def install_importhook(config: Config) -> rewrite.AssertionRewritingHook: diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index e23d89569b0..12c94e99915 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -62,14 +62,14 @@ def __init__(self, config: Config) -> None: self.fnpats = config.getini("python_files") except ValueError: self.fnpats = ["test_*.py", "*_test.py"] - self.session = None # type: Optional[Session] - self._rewritten_names = set() # type: Set[str] - self._must_rewrite = set() # type: Set[str] + self.session: Optional[Session] = None + self._rewritten_names: Set[str] = set() + self._must_rewrite: Set[str] = set() # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, # which might result in infinite recursion (#3506) self._writing_pyc = False self._basenames_to_check_rewrite = {"conftest"} - self._marked_for_rewrite_cache = {} # type: Dict[str, bool] + self._marked_for_rewrite_cache: Dict[str, bool] = {} self._session_paths_checked = False def set_session(self, session: Optional[Session]) -> None: @@ -529,12 +529,12 @@ def _fix(node, lineno, col_offset): def _get_assertion_exprs(src: bytes) -> Dict[int, str]: """Return a mapping from {lineno: "assertion test expression"}.""" - ret = {} # type: Dict[int, str] + ret: Dict[int, str] = {} depth = 0 - lines = [] # type: List[str] - assert_lineno = None # type: Optional[int] - seen_lines = set() # type: Set[int] + lines: List[str] = [] + assert_lineno: Optional[int] = None + seen_lines: Set[int] = set() def _write_and_reset() -> None: nonlocal depth, lines, assert_lineno, seen_lines @@ -699,12 +699,12 @@ def run(self, mod: ast.Module) -> None: ] mod.body[pos:pos] = imports # Collect asserts. - nodes = [mod] # type: List[ast.AST] + nodes: List[ast.AST] = [mod] while nodes: node = nodes.pop() for name, field in ast.iter_fields(node): if isinstance(field, list): - new = [] # type: List[ast.AST] + new: List[ast.AST] = [] for i, child in enumerate(field): if isinstance(child, ast.Assert): # Transform assert. @@ -776,7 +776,7 @@ def push_format_context(self) -> None: to format a string of %-formatted values as added by .explanation_param(). """ - self.explanation_specifiers = {} # type: Dict[str, ast.expr] + self.explanation_specifiers: Dict[str, ast.expr] = {} self.stack.append(self.explanation_specifiers) def pop_format_context(self, expl_expr: ast.expr) -> ast.Name: @@ -828,15 +828,15 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]: lineno=assert_.lineno, ) - self.statements = [] # type: List[ast.stmt] - self.variables = [] # type: List[str] + self.statements: List[ast.stmt] = [] + self.variables: List[str] = [] self.variable_counter = itertools.count() if self.enable_assertion_pass_hook: - self.format_variables = [] # type: List[str] + self.format_variables: List[str] = [] - self.stack = [] # type: List[Dict[str, ast.expr]] - self.expl_stmts = [] # type: List[ast.stmt] + self.stack: List[Dict[str, ast.expr]] = [] + self.expl_stmts: List[ast.stmt] = [] self.push_format_context() # Rewrite assert into a bunch of statements. top_condition, explanation = self.visit(assert_.test) @@ -943,7 +943,7 @@ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: # Process each operand, short-circuiting if needed. for i, v in enumerate(boolop.values): if i: - fail_inner = [] # type: List[ast.stmt] + fail_inner: List[ast.stmt] = [] # cond is set in a prior loop iteration below self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa self.expl_stmts = fail_inner @@ -954,10 +954,10 @@ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: call = ast.Call(app, [expl_format], []) self.expl_stmts.append(ast.Expr(call)) if i < levels: - cond = res # type: ast.expr + cond: ast.expr = res if is_or: cond = ast.UnaryOp(ast.Not(), cond) - inner = [] # type: List[ast.stmt] + inner: List[ast.stmt] = [] self.statements.append(ast.If(cond, inner, [])) self.statements = body = inner self.statements = save @@ -1053,7 +1053,7 @@ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: ast.Tuple(results, ast.Load()), ) if len(comp.ops) > 1: - res = ast.BoolOp(ast.And(), load_names) # type: ast.expr + res: ast.expr = ast.BoolOp(ast.And(), load_names) else: res = load_names[0] return res, self.explanation_param(self.pop_format_context(expl_call)) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 28bd13d4daf..08ff4eacd2b 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -21,11 +21,11 @@ # interpretation code and assertion rewriter to detect this plugin was # loaded and in turn call the hooks defined here as part of the # DebugInterpreter. -_reprcompare = None # type: Optional[Callable[[str, object, object], Optional[str]]] +_reprcompare: Optional[Callable[[str, object, object], Optional[str]]] = None # Works similarly as _reprcompare attribute. Is populated with the hook call # when pytest_runtest_setup is called. -_assertion_pass = None # type: Optional[Callable[[int, str, str], None]] +_assertion_pass: Optional[Callable[[int, str, str], None]] = None def format_explanation(explanation: str) -> str: @@ -197,7 +197,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: """ from difflib import ndiff - explanation = [] # type: List[str] + explanation: List[str] = [] if verbose < 1: i = 0 # just in case left or right has zero length @@ -242,7 +242,7 @@ def _compare_eq_verbose(left: Any, right: Any) -> List[str]: left_lines = repr(left).splitlines(keepends) right_lines = repr(right).splitlines(keepends) - explanation = [] # type: List[str] + explanation: List[str] = [] explanation += ["+" + line for line in left_lines] explanation += ["-" + line for line in right_lines] @@ -296,7 +296,7 @@ def _compare_eq_sequence( left: Sequence[Any], right: Sequence[Any], verbose: int = 0 ) -> List[str]: comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes) - explanation = [] # type: List[str] + explanation: List[str] = [] len_left = len(left) len_right = len(right) for i in range(min(len_left, len_right)): @@ -365,7 +365,7 @@ def _compare_eq_set( def _compare_eq_dict( left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0 ) -> List[str]: - explanation = [] # type: List[str] + explanation: List[str] = [] set_left = set(left) set_right = set(right) common = set_left.intersection(set_right) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 23feb7fbe85..9d548880e34 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -186,7 +186,7 @@ def __init__(self, lfplugin: "LFPlugin") -> None: def pytest_make_collect_report(self, collector: nodes.Collector): if isinstance(collector, Session): out = yield - res = out.get_result() # type: CollectReport + res: CollectReport = out.get_result() # Sort any lf-paths to the beginning. lf_paths = self.lfplugin._last_failed_paths @@ -251,11 +251,9 @@ def __init__(self, config: Config) -> None: active_keys = "lf", "failedfirst" self.active = any(config.getoption(key) for key in active_keys) assert config.cache - self.lastfailed = config.cache.get( - "cache/lastfailed", {} - ) # type: Dict[str, bool] - self._previously_failed_count = None # type: Optional[int] - self._report_status = None # type: Optional[str] + self.lastfailed: Dict[str, bool] = config.cache.get("cache/lastfailed", {}) + self._previously_failed_count: Optional[int] = None + self._report_status: Optional[str] = None self._skipped_files = 0 # count skipped files during collection due to --lf if config.getoption("lf"): @@ -369,8 +367,8 @@ def pytest_collection_modifyitems( yield if self.active: - new_items = order_preserving_dict() # type: Dict[str, nodes.Item] - other_items = order_preserving_dict() # type: Dict[str, nodes.Item] + new_items: Dict[str, nodes.Item] = order_preserving_dict() + other_items: Dict[str, nodes.Item] = order_preserving_dict() for item in items: if item.nodeid not in self.cached_nodeids: new_items[item.nodeid] = item diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index bf3c9894152..dbb6d478f57 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -369,9 +369,7 @@ def __init__(self, targetfd: int) -> None: # Further complications are the need to support suspend() and the # possibility of FD reuse (e.g. the tmpfile getting the very same # target FD). The following approach is robust, I believe. - self.targetfd_invalid = os.open( - os.devnull, os.O_RDWR - ) # type: Optional[int] + self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR) os.dup2(self.targetfd_invalid, targetfd) else: self.targetfd_invalid = None @@ -505,8 +503,8 @@ class CaptureResult(Generic[AnyStr]): __slots__ = ("out", "err") def __init__(self, out: AnyStr, err: AnyStr) -> None: - self.out = out # type: AnyStr - self.err = err # type: AnyStr + self.out: AnyStr = out + self.err: AnyStr = err def __len__(self) -> int: return 2 @@ -665,8 +663,8 @@ class CaptureManager: def __init__(self, method: "_CaptureMethod") -> None: self._method = method - self._global_capturing = None # type: Optional[MultiCapture[str]] - self._capture_fixture = None # type: Optional[CaptureFixture[Any]] + self._global_capturing: Optional[MultiCapture[str]] = None + self._capture_fixture: Optional[CaptureFixture[Any]] = None def __repr__(self) -> str: return "".format( @@ -835,7 +833,7 @@ class CaptureFixture(Generic[AnyStr]): def __init__(self, captureclass, request: SubRequest) -> None: self.captureclass = captureclass self.request = request - self._capture = None # type: Optional[MultiCapture[AnyStr]] + self._capture: Optional[MultiCapture[AnyStr]] = None self._captured_out = self.captureclass.EMPTY_BUFFER self._captured_err = self.captureclass.EMPTY_BUFFER diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 69ff2e0074e..f704a990e33 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -36,7 +36,7 @@ # https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions class NotSetType(enum.Enum): token = 0 -NOTSET = NotSetType.token # type: Final # noqa: E305 +NOTSET: "Final" = NotSetType.token # noqa: E305 # fmt: on if sys.version_info >= (3, 8): diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 08d37650c10..39fbe01a064 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -159,9 +159,9 @@ def main( return ExitCode.USAGE_ERROR else: try: - ret = config.hook.pytest_cmdline_main( + ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main( config=config - ) # type: Union[ExitCode, int] + ) try: return ExitCode(ret) except ValueError: @@ -337,27 +337,27 @@ def __init__(self) -> None: super().__init__("pytest") # The objects are module objects, only used generically. - self._conftest_plugins = set() # type: Set[types.ModuleType] + self._conftest_plugins: Set[types.ModuleType] = set() # State related to local conftest plugins. - self._dirpath2confmods = {} # type: Dict[py.path.local, List[types.ModuleType]] - self._conftestpath2mod = {} # type: Dict[Path, types.ModuleType] - self._confcutdir = None # type: Optional[py.path.local] + self._dirpath2confmods: Dict[py.path.local, List[types.ModuleType]] = {} + self._conftestpath2mod: Dict[Path, types.ModuleType] = {} + self._confcutdir: Optional[py.path.local] = None self._noconftest = False - self._duplicatepaths = set() # type: Set[py.path.local] + self._duplicatepaths: Set[py.path.local] = set() # plugins that were explicitly skipped with pytest.skip # list of (module name, skip reason) # previously we would issue a warning when a plugin was skipped, but # since we refactored warnings as first citizens of Config, they are # just stored here to be used later. - self.skipped_plugins = [] # type: List[Tuple[str, str]] + self.skipped_plugins: List[Tuple[str, str]] = [] self.add_hookspecs(_pytest.hookspec) self.register(self) if os.environ.get("PYTEST_DEBUG"): - err = sys.stderr # type: IO[str] - encoding = getattr(err, "encoding", "utf8") # type: str + err: IO[str] = sys.stderr + encoding: str = getattr(err, "encoding", "utf8") try: err = open( os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding, @@ -431,7 +431,7 @@ def register( ) ) return None - ret = super().register(plugin, name) # type: Optional[str] + ret: Optional[str] = super().register(plugin, name) if ret: self.hook.pytest_plugin_registered.call_historic( kwargs=dict(plugin=plugin, manager=self) @@ -443,7 +443,7 @@ def register( def getplugin(self, name: str): # Support deprecated naming because plugins (xdist e.g.) use it. - plugin = self.get_plugin(name) # type: Optional[_PluggyPlugin] + plugin: Optional[_PluggyPlugin] = self.get_plugin(name) return plugin def hasplugin(self, name: str) -> bool: @@ -898,10 +898,10 @@ def __init__( self.trace = self.pluginmanager.trace.root.get("config") self.hook = self.pluginmanager.hook - self._inicache = {} # type: Dict[str, Any] - self._override_ini = () # type: Sequence[str] - self._opt2dest = {} # type: Dict[str, str] - self._cleanup = [] # type: List[Callable[[], None]] + self._inicache: Dict[str, Any] = {} + self._override_ini: Sequence[str] = () + self._opt2dest: Dict[str, str] = {} + self._cleanup: List[Callable[[], None]] = [] # A place where plugins can store information on the config for their # own use. Currently only intended for internal plugins. self._store = Store() @@ -914,7 +914,7 @@ def __init__( if TYPE_CHECKING: from _pytest.cacheprovider import Cache - self.cache = None # type: Optional[Cache] + self.cache: Optional[Cache] = None @property def invocation_dir(self) -> py.path.local: @@ -989,9 +989,9 @@ def _ensure_unconfigure(self) -> None: fin() def get_terminal_writer(self) -> TerminalWriter: - terminalreporter = self.pluginmanager.get_plugin( + terminalreporter: TerminalReporter = self.pluginmanager.get_plugin( "terminalreporter" - ) # type: TerminalReporter + ) return terminalreporter._tw def pytest_cmdline_parse( @@ -1026,7 +1026,7 @@ def notify_exception( option: Optional[argparse.Namespace] = None, ) -> None: if option and getattr(option, "fulltrace", False): - style = "long" # type: _TracebackStyle + style: _TracebackStyle = "long" else: style = "native" excrepr = excinfo.getrepr( @@ -1415,7 +1415,7 @@ def _getconftest_pathlist( except KeyError: return None modpath = py.path.local(mod.__file__).dirpath() - values = [] # type: List[py.path.local] + values: List[py.path.local] = [] for relroot in relroots: if not isinstance(relroot, py.path.local): relroot = relroot.replace("/", os.sep) @@ -1568,7 +1568,7 @@ def parse_warning_filter( while len(parts) < 5: parts.append("") action_, message, category_, module, lineno_ = [s.strip() for s in parts] - action = warnings._getaction(action_) # type: str # type: ignore[attr-defined] + action: str = warnings._getaction(action_) # type: ignore[attr-defined] category: Type[Warning] = warnings._getcategory(category_) # type: ignore[attr-defined] if message and escape: message = re.escape(message) diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 3ee54a552e0..86e9c990aed 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -35,7 +35,7 @@ class Parser: there's an error processing the command line arguments. """ - prog = None # type: Optional[str] + prog: Optional[str] = None def __init__( self, @@ -43,12 +43,12 @@ def __init__( processopt: Optional[Callable[["Argument"], None]] = None, ) -> None: self._anonymous = OptionGroup("custom options", parser=self) - self._groups = [] # type: List[OptionGroup] + self._groups: List[OptionGroup] = [] self._processopt = processopt self._usage = usage - self._inidict = {} # type: Dict[str, Tuple[str, Optional[str], Any]] - self._ininames = [] # type: List[str] - self.extra_info = {} # type: Dict[str, Any] + self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {} + self._ininames: List[str] = [] + self.extra_info: Dict[str, Any] = {} def processoption(self, option: "Argument") -> None: if self._processopt: @@ -207,8 +207,8 @@ class Argument: def __init__(self, *names: str, **attrs: Any) -> None: """Store parms in private vars for use in add_argument.""" self._attrs = attrs - self._short_opts = [] # type: List[str] - self._long_opts = [] # type: List[str] + self._short_opts: List[str] = [] + self._long_opts: List[str] = [] if "%default" in (attrs.get("help") or ""): warnings.warn( 'pytest now uses argparse. "%default" should be' @@ -254,7 +254,7 @@ def __init__(self, *names: str, **attrs: Any) -> None: except KeyError: pass self._set_opt_strings(names) - dest = attrs.get("dest") # type: Optional[str] + dest: Optional[str] = attrs.get("dest") if dest: self.dest = dest elif self._long_opts: @@ -315,7 +315,7 @@ def _set_opt_strings(self, opts: Sequence[str]) -> None: self._long_opts.append(opt) def __repr__(self) -> str: - args = [] # type: List[str] + args: List[str] = [] if self._short_opts: args += ["_short_opts: " + repr(self._short_opts)] if self._long_opts: @@ -334,7 +334,7 @@ def __init__( ) -> None: self.name = name self.description = description - self.options = [] # type: List[Argument] + self.options: List[Argument] = [] self.parser = parser def addoption(self, *optnames: str, **attrs: Any) -> None: @@ -472,9 +472,7 @@ def _format_action_invocation(self, action: argparse.Action) -> str: orgstr = argparse.HelpFormatter._format_action_invocation(self, action) if orgstr and orgstr[0] != "-": # only optional arguments return orgstr - res = getattr( - action, "_formatted_action_invocation", None - ) # type: Optional[str] + res: Optional[str] = getattr(action, "_formatted_action_invocation", None) if res: return res options = orgstr.split(", ") @@ -483,7 +481,7 @@ def _format_action_invocation(self, action: argparse.Action) -> str: action._formatted_action_invocation = orgstr # type: ignore return orgstr return_list = [] - short_long = {} # type: Dict[str, str] + short_long: Dict[str, str] = {} for option in options: if len(option) == 2 or option[2] == " ": continue diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 8327e844924..04fa8f37540 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -110,7 +110,7 @@ def locate_config( def get_common_ancestor(paths: Iterable[Path]) -> Path: - common_ancestor = None # type: Optional[Path] + common_ancestor: Optional[Path] = None for path in paths: if not path.exists(): continue @@ -175,7 +175,7 @@ def determine_setup( dirs = get_dirs_from_args(args) if inifile: inipath_ = absolutepath(inifile) - inipath = inipath_ # type: Optional[Path] + inipath: Optional[Path] = inipath_ inicfg = load_config_dict_from_file(inipath_) or {} if rootdir_cmd_arg is None: rootdir = get_common_ancestor(dirs) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 80004f468ee..d3a5c6173f3 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -94,13 +94,13 @@ def fin() -> None: class pytestPDB: """Pseudo PDB that defers to the real pdb.""" - _pluginmanager = None # type: Optional[PytestPluginManager] - _config = None # type: Config - _saved = ( - [] - ) # type: List[Tuple[Callable[..., None], Optional[PytestPluginManager], Config]] + _pluginmanager: Optional[PytestPluginManager] = None + _config: Optional[Config] = None + _saved: List[ + Tuple[Callable[..., None], Optional[PytestPluginManager], Optional[Config]] + ] = [] _recursive_debug = 0 - _wrapped_pdb_cls = None # type: Optional[Tuple[Type[Any], Type[Any]]] + _wrapped_pdb_cls: Optional[Tuple[Type[Any], Type[Any]]] = None @classmethod def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]: @@ -166,6 +166,7 @@ def do_debug(self, arg): def do_continue(self, arg): ret = super().do_continue(arg) if cls._recursive_debug == 0: + assert cls._config is not None tw = _pytest.config.create_terminal_writer(cls._config) tw.line() @@ -239,7 +240,7 @@ def _init_pdb(cls, method, *args, **kwargs): import _pytest.config if cls._pluginmanager is None: - capman = None # type: Optional[CaptureManager] + capman: Optional[CaptureManager] = None else: capman = cls._pluginmanager.getplugin("capturemanager") if capman: diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 194e5e59896..fd9434a9215 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -59,7 +59,7 @@ # Lazy definition of runner class RUNNER_CLASS = None # Lazy definition of output checker class -CHECKER_CLASS = None # type: Optional[Type[doctest.OutputChecker]] +CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None def pytest_addoption(parser: Parser) -> None: @@ -124,10 +124,10 @@ def pytest_collect_file( config = parent.config if path.ext == ".py": if config.option.doctestmodules and not _is_setup_py(path): - mod = DoctestModule.from_parent(parent, fspath=path) # type: DoctestModule + mod: DoctestModule = DoctestModule.from_parent(parent, fspath=path) return mod elif _is_doctest(config, path, parent): - txt = DoctestTextfile.from_parent(parent, fspath=path) # type: DoctestTextfile + txt: DoctestTextfile = DoctestTextfile.from_parent(parent, fspath=path) return txt return None @@ -163,7 +163,7 @@ def toterminal(self, tw: TerminalWriter) -> None: class MultipleDoctestFailures(Exception): - def __init__(self, failures: "Sequence[doctest.DocTestFailure]") -> None: + def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None: super().__init__() self.failures = failures @@ -180,7 +180,7 @@ class PytestDoctestRunner(doctest.DebugRunner): def __init__( self, - checker: Optional[doctest.OutputChecker] = None, + checker: Optional["doctest.OutputChecker"] = None, verbose: Optional[bool] = None, optionflags: int = 0, continue_on_failure: bool = True, @@ -251,7 +251,7 @@ def __init__( self.runner = runner self.dtest = dtest self.obj = None - self.fixture_request = None # type: Optional[FixtureRequest] + self.fixture_request: Optional[FixtureRequest] = None @classmethod def from_parent( # type: ignore @@ -281,7 +281,7 @@ def runtest(self) -> None: assert self.runner is not None _check_all_skipped(self.dtest) self._disable_output_capturing_for_darwin() - failures = [] # type: List[doctest.DocTestFailure] + failures: List["doctest.DocTestFailure"] = [] # Type ignored because we change the type of `out` from what # doctest expects. self.runner.run(self.dtest, out=failures) # type: ignore[arg-type] @@ -305,9 +305,9 @@ def repr_failure( # type: ignore[override] ) -> Union[str, TerminalRepr]: import doctest - failures = ( - None - ) # type: Optional[Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]] + failures: Optional[ + Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]] + ] = (None) if isinstance( excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException) ): @@ -636,8 +636,8 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: return got offset = 0 for w, g in zip(wants, gots): - fraction = w.group("fraction") # type: Optional[str] - exponent = w.group("exponent1") # type: Optional[str] + fraction: Optional[str] = w.group("fraction") + exponent: Optional[str] = w.group("exponent1") if exponent is None: exponent = w.group("exponent2") if fraction is None: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 00dfb2559b9..f00f534d8df 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -160,8 +160,8 @@ def add_funcarg_pseudo_fixture_def( # This function call does not have direct parametrization. return # Collect funcargs of all callspecs into a list of values. - arg2params = {} # type: Dict[str, List[object]] - arg2scope = {} # type: Dict[str, _Scope] + arg2params: Dict[str, List[object]] = {} + arg2scope: Dict[str, _Scope] = {} for callspec in metafunc._calls: for argname, argvalue in callspec.funcargs.items(): assert argname not in callspec.params @@ -219,9 +219,9 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: """Return fixturemarker or None if it doesn't exist or raised exceptions.""" try: - fixturemarker = getattr( + fixturemarker: Optional[FixtureFunctionMarker] = getattr( obj, "_pytestfixturefunction", None - ) # type: Optional[FixtureFunctionMarker] + ) except TEST_OUTCOME: # some objects raise errors like request (from flask import request) # we don't expect them to be fixture functions @@ -242,7 +242,7 @@ def get_parametrized_fixture_keys(item: nodes.Item, scopenum: int) -> Iterator[_ except AttributeError: pass else: - cs = callspec # type: CallSpec2 + cs: CallSpec2 = callspec # cs.indices.items() is random order of argnames. Need to # sort this so that different calls to # get_parametrized_fixture_keys will be deterministic. @@ -250,7 +250,7 @@ def get_parametrized_fixture_keys(item: nodes.Item, scopenum: int) -> Iterator[_ if cs._arg2scopenum[argname] != scopenum: continue if scopenum == 0: # session - key = (argname, param_index) # type: _Key + key: _Key = (argname, param_index) elif scopenum == 1: # package key = (argname, param_index, item.fspath.dirpath()) elif scopenum == 2: # module @@ -268,12 +268,12 @@ def get_parametrized_fixture_keys(item: nodes.Item, scopenum: int) -> Iterator[_ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]: - argkeys_cache = {} # type: Dict[int, Dict[nodes.Item, Dict[_Key, None]]] - items_by_argkey = {} # type: Dict[int, Dict[_Key, Deque[nodes.Item]]] + argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]] = {} + items_by_argkey: Dict[int, Dict[_Key, Deque[nodes.Item]]] = {} for scopenum in range(0, scopenum_function): - d = {} # type: Dict[nodes.Item, Dict[_Key, None]] + d: Dict[nodes.Item, Dict[_Key, None]] = {} argkeys_cache[scopenum] = d - item_d = defaultdict(deque) # type: Dict[_Key, Deque[nodes.Item]] + item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque) items_by_argkey[scopenum] = item_d for item in items: # cast is a workaround for https://github.com/python/typeshed/issues/3800. @@ -312,13 +312,13 @@ def reorder_items_atscope( ) -> Dict[nodes.Item, None]: if scopenum >= scopenum_function or len(items) < 3: return items - ignore = set() # type: Set[Optional[_Key]] + ignore: Set[Optional[_Key]] = set() items_deque = deque(items) - items_done = order_preserving_dict() # type: Dict[nodes.Item, None] + items_done: Dict[nodes.Item, None] = order_preserving_dict() scoped_items_by_argkey = items_by_argkey[scopenum] scoped_argkeys_cache = argkeys_cache[scopenum] while items_deque: - no_argkey_group = order_preserving_dict() # type: Dict[nodes.Item, None] + no_argkey_group: Dict[nodes.Item, None] = order_preserving_dict() slicing_argkey = None while items_deque: item = items_deque.popleft() @@ -400,7 +400,7 @@ def prune_dependency_tree(self) -> None: tree. In this way the dependency tree can get pruned, and the closure of argnames may get reduced. """ - closure = set() # type: Set[str] + closure: Set[str] = set() working_set = set(self.initialnames) while working_set: argname = working_set.pop() @@ -428,16 +428,14 @@ class FixtureRequest: def __init__(self, pyfuncitem) -> None: self._pyfuncitem = pyfuncitem #: Fixture for which this request is being performed. - self.fixturename = None # type: Optional[str] + self.fixturename: Optional[str] = None #: Scope string, one of "function", "class", "module", "session". - self.scope = "function" # type: _Scope - self._fixture_defs = {} # type: Dict[str, FixtureDef[Any]] - fixtureinfo = pyfuncitem._fixtureinfo # type: FuncFixtureInfo + self.scope: _Scope = "function" + self._fixture_defs: Dict[str, FixtureDef[Any]] = {} + fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() - self._arg2index = {} # type: Dict[str, int] - self._fixturemanager = ( - pyfuncitem.session._fixturemanager - ) # type: FixtureManager + self._arg2index: Dict[str, int] = {} + self._fixturemanager: FixtureManager = (pyfuncitem.session._fixturemanager) @property def fixturenames(self) -> List[str]: @@ -589,7 +587,7 @@ def _get_active_fixturedef( except FixtureLookupError: if argname == "request": cached_result = (self, [0], None) - scope = "function" # type: _Scope + scope: _Scope = "function" return PseudoFixtureDef(cached_result, scope) raise # Remove indent to prevent the python3 exception @@ -600,7 +598,7 @@ def _get_active_fixturedef( def _get_fixturestack(self) -> List["FixtureDef[Any]"]: current = self - values = [] # type: List[FixtureDef[Any]] + values: List[FixtureDef[Any]] = [] while 1: fixturedef = getattr(current, "_fixturedef", None) if fixturedef is None: @@ -782,7 +780,7 @@ def _schedule_finalizers( super()._schedule_finalizers(fixturedef, subrequest) -scopes = ["session", "package", "module", "class", "function"] # type: List[_Scope] +scopes: List["_Scope"] = ["session", "package", "module", "class", "function"] scopenum_function = scopes.index("function") @@ -793,7 +791,7 @@ def scopemismatch(currentscope: "_Scope", newscope: "_Scope") -> bool: def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int: """Look up the index of ``scope`` and raise a descriptive value error if not defined.""" - strscopes = scopes # type: Sequence[str] + strscopes: Sequence[str] = scopes try: return strscopes.index(scope) except ValueError: @@ -818,7 +816,7 @@ def __init__( self.msg = msg def formatrepr(self) -> "FixtureLookupErrorRepr": - tblines = [] # type: List[str] + tblines: List[str] = [] addline = tblines.append stack = [self.request._pyfuncitem.obj] stack.extend(map(lambda x: x.func, self.fixturestack)) @@ -995,14 +993,14 @@ def __init__( where=baseid, ) self.scope = scope_ - self.params = params # type: Optional[Sequence[object]] - self.argnames = getfuncargnames( + self.params: Optional[Sequence[object]] = params + self.argnames: Tuple[str, ...] = getfuncargnames( func, name=argname, is_method=unittest - ) # type: Tuple[str, ...] + ) self.unittest = unittest self.ids = ids - self.cached_result = None # type: Optional[_FixtureCachedResult[_FixtureValue]] - self._finalizers = [] # type: List[Callable[[], object]] + self.cached_result: Optional[_FixtureCachedResult[_FixtureValue]] = None + self._finalizers: List[Callable[[], object]] = [] def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._finalizers.append(finalizer) @@ -1408,12 +1406,12 @@ class FixtureManager: def __init__(self, session: "Session") -> None: self.session = session - self.config = session.config # type: Config - self._arg2fixturedefs = {} # type: Dict[str, List[FixtureDef[Any]]] - self._holderobjseen = set() # type: Set[object] - self._nodeid_and_autousenames = [ + self.config: Config = session.config + self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {} + self._holderobjseen: Set[object] = set() + self._nodeid_and_autousenames: List[Tuple[str, List[str]]] = [ ("", self.config.getini("usefixtures")) - ] # type: List[Tuple[str, List[str]]] + ] session.config.pluginmanager.register(self, "funcmanage") def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]: @@ -1425,7 +1423,7 @@ def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]: These things are done later as well when dealing with parametrization so this could be improved. """ - parametrize_argnames = [] # type: List[str] + parametrize_argnames: List[str] = [] for marker in node.iter_markers(name="parametrize"): if not marker.kwargs.get("indirect", False): p_argnames, _ = ParameterSet._parse_parametrize_args( @@ -1477,7 +1475,7 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: def _getautousenames(self, nodeid: str) -> List[str]: """Return a list of fixture names to be used.""" - autousenames = [] # type: List[str] + autousenames: List[str] = [] for baseid, basenames in self._nodeid_and_autousenames: if nodeid.startswith(baseid): if baseid: @@ -1516,7 +1514,7 @@ def merge(otherlist: Iterable[str]) -> None: # need to return it as well, so save this. initialnames = tuple(fixturenames_closure) - arg2fixturedefs = {} # type: Dict[str, Sequence[FixtureDef[Any]]] + arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {} lastlen = -1 while lastlen != len(fixturenames_closure): lastlen = len(fixturenames_closure) diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 9c3a1804d5f..4384d07b261 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -97,7 +97,7 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(hookwrapper=True) def pytest_cmdline_parse(): outcome = yield - config = outcome.get_result() # type: Config + config: Config = outcome.get_result() if config.option.debug: path = os.path.abspath("pytestdebug.log") debugfile = open(path, "w") diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 621d631768b..c4761cd3b87 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -93,9 +93,9 @@ def __init__(self, nodeid: Union[str, TestReport], xml: "LogXML") -> None: self.add_stats = self.xml.add_stats self.family = self.xml.family self.duration = 0 - self.properties = [] # type: List[Tuple[str, str]] - self.nodes = [] # type: List[ET.Element] - self.attrs = {} # type: Dict[str, str] + self.properties: List[Tuple[str, str]] = [] + self.nodes: List[ET.Element] = [] + self.attrs: Dict[str, str] = {} def append(self, node: ET.Element) -> None: self.xml.add_stats(node.tag) @@ -122,11 +122,11 @@ def record_testreport(self, testreport: TestReport) -> None: classnames = names[:-1] if self.xml.prefix: classnames.insert(0, self.xml.prefix) - attrs = { + attrs: Dict[str, str] = { "classname": ".".join(classnames), "name": bin_xml_escape(names[-1]), "file": testreport.location[0], - } # type: Dict[str, str] + } if testreport.location[1] is not None: attrs["line"] = str(testreport.location[1]) if hasattr(testreport, "url"): @@ -199,9 +199,9 @@ def append_failure(self, report: TestReport) -> None: self._add_simple("skipped", "xfail-marked test passes unexpectedly") else: assert report.longrepr is not None - reprcrash = getattr( + reprcrash: Optional[ReprFileLocation] = getattr( report.longrepr, "reprcrash", None - ) # type: Optional[ReprFileLocation] + ) if reprcrash is not None: message = reprcrash.message else: @@ -219,9 +219,9 @@ def append_collect_skipped(self, report: TestReport) -> None: def append_error(self, report: TestReport) -> None: assert report.longrepr is not None - reprcrash = getattr( + reprcrash: Optional[ReprFileLocation] = getattr( report.longrepr, "reprcrash", None - ) # type: Optional[ReprFileLocation] + ) if reprcrash is not None: reason = reprcrash.message else: @@ -481,17 +481,17 @@ def __init__( self.log_passing_tests = log_passing_tests self.report_duration = report_duration self.family = family - self.stats = dict.fromkeys( + self.stats: Dict[str, int] = dict.fromkeys( ["error", "passed", "failure", "skipped"], 0 - ) # type: Dict[str, int] - self.node_reporters = ( - {} - ) # type: Dict[Tuple[Union[str, TestReport], object], _NodeReporter] - self.node_reporters_ordered = [] # type: List[_NodeReporter] - self.global_properties = [] # type: List[Tuple[str, str]] + ) + self.node_reporters: Dict[ + Tuple[Union[str, TestReport], object], _NodeReporter + ] = ({}) + self.node_reporters_ordered: List[_NodeReporter] = [] + self.global_properties: List[Tuple[str, str]] = [] # List of reports that failed on call but teardown is pending. - self.open_reports = [] # type: List[TestReport] + self.open_reports: List[TestReport] = [] self.cnt_double_fail_tests = 0 # Replaces convenience family with real family. @@ -507,7 +507,7 @@ def finalize(self, report: TestReport) -> None: reporter.finalize() def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter: - nodeid = getattr(report, "nodeid", report) # type: Union[str, TestReport] + nodeid: Union[str, TestReport] = getattr(report, "nodeid", report) # Local hack to handle xdist report order. workernode = getattr(report, "node", None) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 904e27ee4b3..3b046c95441 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -47,7 +47,7 @@ class ColoredLevelFormatter(logging.Formatter): """A logging formatter which colorizes the %(levelname)..s part of the log format passed to __init__.""" - LOGLEVEL_COLOROPTS = { + LOGLEVEL_COLOROPTS: Mapping[int, AbstractSet[str]] = { logging.CRITICAL: {"red"}, logging.ERROR: {"red", "bold"}, logging.WARNING: {"yellow"}, @@ -55,13 +55,13 @@ class ColoredLevelFormatter(logging.Formatter): logging.INFO: {"green"}, logging.DEBUG: {"purple"}, logging.NOTSET: set(), - } # type: Mapping[int, AbstractSet[str]] + } LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*s)") def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._original_fmt = self._style._fmt - self._level_to_fmt_mapping = {} # type: Dict[int, str] + self._level_to_fmt_mapping: Dict[int, str] = {} assert self._fmt is not None levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt) @@ -315,12 +315,12 @@ def __exit__(self, type, value, traceback): class LogCaptureHandler(logging.StreamHandler): """A logging handler that stores log records and the log text.""" - stream = None # type: StringIO + stream: StringIO def __init__(self) -> None: """Create a new log handler.""" super().__init__(StringIO()) - self.records = [] # type: List[logging.LogRecord] + self.records: List[logging.LogRecord] = [] def emit(self, record: logging.LogRecord) -> None: """Keep the log records in a list in addition to the log text.""" @@ -346,9 +346,9 @@ class LogCaptureFixture: def __init__(self, item: nodes.Node) -> None: self._item = item - self._initial_handler_level = None # type: Optional[int] + self._initial_handler_level: Optional[int] = None # Dict of log name -> log level. - self._initial_logger_levels = {} # type: Dict[Optional[str], int] + self._initial_logger_levels: Dict[Optional[str], int] = {} def _finalize(self) -> None: """Finalize the fixture. @@ -564,9 +564,9 @@ def __init__(self, config: Config) -> None: terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") capture_manager = config.pluginmanager.get_plugin("capturemanager") # if capturemanager plugin is disabled, live logging still works. - self.log_cli_handler = _LiveLoggingStreamHandler( - terminal_reporter, capture_manager - ) # type: Union[_LiveLoggingStreamHandler, _LiveLoggingNullHandler] + self.log_cli_handler: Union[ + _LiveLoggingStreamHandler, _LiveLoggingNullHandler + ] = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) else: self.log_cli_handler = _LiveLoggingNullHandler() log_cli_formatter = self._create_formatter( @@ -582,9 +582,9 @@ def _create_formatter(self, log_format, log_date_format, auto_indent): if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search( log_format ): - formatter = ColoredLevelFormatter( + formatter: logging.Formatter = ColoredLevelFormatter( create_terminal_writer(self._config), log_format, log_date_format - ) # type: logging.Formatter + ) else: formatter = logging.Formatter(log_format, log_date_format) @@ -699,7 +699,7 @@ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, Non def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("setup") - empty = {} # type: Dict[str, List[logging.LogRecord]] + empty: Dict[str, List[logging.LogRecord]] = {} item._store[caplog_records_key] = empty yield from self._runtest_for(item, "setup") @@ -755,7 +755,7 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): # Officially stream needs to be a IO[str], but TerminalReporter # isn't. So force it. - stream = None # type: TerminalReporter # type: ignore + stream: TerminalReporter = None # type: ignore def __init__( self, diff --git a/src/_pytest/main.py b/src/_pytest/main.py index bb08bb15c16..d8a208a1a2e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -262,7 +262,7 @@ def wrap_session( session.exitstatus = ExitCode.TESTS_FAILED except (KeyboardInterrupt, exit.Exception): excinfo = _pytest._code.ExceptionInfo.from_current() - exitstatus = ExitCode.INTERRUPTED # type: Union[int, ExitCode] + exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED if isinstance(excinfo.value, exit.Exception): if excinfo.value.returncode is not None: exitstatus = excinfo.value.returncode @@ -439,10 +439,10 @@ class Session(nodes.FSCollector): Interrupted = Interrupted Failed = Failed # Set on the session by runner.pytest_sessionstart. - _setupstate = None # type: SetupState + _setupstate: SetupState # Set on the session by fixtures.pytest_sessionstart. - _fixturemanager = None # type: FixtureManager - exitstatus = None # type: Union[int, ExitCode] + _fixturemanager: FixtureManager + exitstatus: Union[int, ExitCode] def __init__(self, config: Config) -> None: super().__init__( @@ -450,21 +450,19 @@ def __init__(self, config: Config) -> None: ) self.testsfailed = 0 self.testscollected = 0 - self.shouldstop = False # type: Union[bool, str] - self.shouldfail = False # type: Union[bool, str] + self.shouldstop: Union[bool, str] = False + self.shouldfail: Union[bool, str] = False self.trace = config.trace.root.get("collection") self.startdir = config.invocation_dir - self._initialpaths = frozenset() # type: FrozenSet[py.path.local] + self._initialpaths: FrozenSet[py.path.local] = frozenset() - self._bestrelpathcache = _bestrelpath_cache( - config.rootpath - ) # type: Dict[Path, str] + self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath) self.config.pluginmanager.register(self, name="session") @classmethod def from_config(cls, config: Config) -> "Session": - session = cls._create(config) # type: Session + session: Session = cls._create(config) return session def __repr__(self) -> str: @@ -589,15 +587,15 @@ def perform_collect( self.trace("perform_collect", self, args) self.trace.root.indent += 1 - self._notfound = [] # type: List[Tuple[str, Sequence[nodes.Collector]]] - self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]] - self.items = [] # type: List[nodes.Item] + self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = [] + self._initial_parts: List[Tuple[py.path.local, List[str]]] = [] + self.items: List[nodes.Item] = [] hook = self.config.hook - items = self.items # type: Sequence[Union[nodes.Item, nodes.Collector]] + items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items try: - initialpaths = [] # type: List[py.path.local] + initialpaths: List[py.path.local] = [] for arg in args: fspath, parts = resolve_collection_argument( self.config.invocation_params.dir, @@ -637,19 +635,17 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: from _pytest.python import Package # Keep track of any collected nodes in here, so we don't duplicate fixtures. - node_cache1 = {} # type: Dict[py.path.local, Sequence[nodes.Collector]] - node_cache2 = ( - {} - ) # type: Dict[Tuple[Type[nodes.Collector], py.path.local], nodes.Collector] + node_cache1: Dict[py.path.local, Sequence[nodes.Collector]] = {} + node_cache2: Dict[ + Tuple[Type[nodes.Collector], py.path.local], nodes.Collector + ] = ({}) # Keep track of any collected collectors in matchnodes paths, so they # are not collected more than once. - matchnodes_cache = ( - {} - ) # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport] + matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = ({}) # Dirnames of pkgs with dunder-init files. - pkg_roots = {} # type: Dict[str, Package] + pkg_roots: Dict[str, Package] = {} for argpath, names in self._initial_parts: self.trace("processing argument", (argpath, names)) @@ -678,7 +674,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: if argpath.check(dir=1): assert not names, "invalid arg {!r}".format((argpath, names)) - seen_dirs = set() # type: Set[py.path.local] + seen_dirs: Set[py.path.local] = set() for direntry in visit(str(argpath), self._recurse): if not direntry.is_file(): continue @@ -718,9 +714,9 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: node_cache1[argpath] = col matching = [] - work = [ - (col, names) - ] # type: List[Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]]] + work: List[ + Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]] + ] = [(col, names)] while work: self.trace("matchnodes", col, names) self.trace.root.indent += 1 diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index b3acef5d010..dc3991b10c4 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -133,7 +133,7 @@ def reject(self, expected: Sequence[TokenType]) -> "NoReturn": def expression(s: Scanner) -> ast.Expression: if s.accept(TokenType.EOF): - ret = ast.NameConstant(False) # type: ast.expr + ret: ast.expr = ast.NameConstant(False) else: ret = expr(s) s.accept(TokenType.EOF, reject=True) @@ -203,9 +203,9 @@ def compile(self, input: str) -> "Expression": :param input: The input expression - one line. """ astexpr = expression(Scanner(input)) - code = compile( + code: types.CodeType = compile( astexpr, filename="", mode="eval", - ) # type: types.CodeType + ) return Expression(code) def evaluate(self, matcher: Callable[[str], bool]) -> bool: @@ -217,7 +217,5 @@ def evaluate(self, matcher: Callable[[str], bool]) -> bool: :returns: Whether the expression matches or not. """ - ret = eval( - self.code, {"__builtins__": {}}, MatcherAdapter(matcher) - ) # type: bool + ret: bool = eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher)) return ret diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index b2ab2e35be1..6cbdf8b3066 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -127,7 +127,7 @@ def extract_from( return cls.param(parameterset) else: # TODO: Refactor to fix this type-ignore. Currently the following - # type-checks but crashes: + # passes type-checking but crashes: # # @pytest.mark.parametrize(('x', 'y'), [1, 2]) # def test_foo(x, y): pass @@ -231,7 +231,7 @@ def combined_with(self, other: "Mark") -> "Mark": assert self.name == other.name # Remember source of ids with parametrize Marks. - param_ids_from = None # type: Optional[Mark] + param_ids_from: Optional[Mark] = None if self.name == "parametrize": if other._has_param_ids(): param_ids_from = other @@ -465,8 +465,8 @@ def test_function(): applies a 'slowtest' :class:`Mark` on ``test_function``. """ - _config = None # type: Optional[Config] - _markers = set() # type: Set[str] + _config: Optional[Config] = None + _markers: Set[str] = set() # See TYPE_CHECKING above. if TYPE_CHECKING: diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index d75032e65a2..df4726705d1 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -115,12 +115,10 @@ class MonkeyPatch: setattr/item/env/syspath changes.""" def __init__(self) -> None: - self._setattr = [] # type: List[Tuple[object, str, object]] - self._setitem = ( - [] - ) # type: List[Tuple[MutableMapping[Any, Any], object, object]] - self._cwd = None # type: Optional[str] - self._savesyspath = None # type: Optional[List[str]] + self._setattr: List[Tuple[object, str, object]] = [] + self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = ([]) + self._cwd: Optional[str] = None + self._savesyspath: Optional[List[str]] = None @contextmanager def context(self) -> Generator["MonkeyPatch", None, None]: @@ -292,7 +290,7 @@ def delenv(self, name: str, raising: bool = True) -> None: Raises ``KeyError`` if it does not exist, unless ``raising`` is set to False. """ - environ = os.environ # type: MutableMapping[str, str] + environ: MutableMapping[str, str] = os.environ self.delitem(environ, name, raising=raising) def syspath_prepend(self, path) -> None: diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 96bac46af5c..6ab08953a7a 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -140,7 +140,7 @@ def __init__( #: The pytest config object. if config: - self.config = config # type: Config + self.config: Config = config else: if not parent: raise TypeError("config or parent must be provided") @@ -161,10 +161,10 @@ def __init__( self.keywords = NodeKeywords(self) #: The marker objects belonging to this node. - self.own_markers = [] # type: List[Mark] + self.own_markers: List[Mark] = [] #: Allow adding of extra keywords to use for matching. - self.extra_keyword_matches = set() # type: Set[str] + self.extra_keyword_matches: Set[str] = set() if nodeid is not None: assert "::()" not in nodeid @@ -256,7 +256,7 @@ def listchain(self) -> List["Node"]: """Return list of all parent collectors up to self, starting from the root of collection tree.""" chain = [] - item = self # type: Optional[Node] + item: Optional[Node] = self while item is not None: chain.append(item) item = item.parent @@ -326,7 +326,7 @@ def get_closest_marker( def listextrakeywords(self) -> Set[str]: """Return a set of all extra keywords in self and any parents.""" - extra_keywords = set() # type: Set[str] + extra_keywords: Set[str] = set() for item in self.listchain(): extra_keywords.update(item.extra_keyword_matches) return extra_keywords @@ -345,7 +345,7 @@ def addfinalizer(self, fin: Callable[[], object]) -> None: def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]: """Get the next parent node (including self) which is an instance of the given class.""" - current = self # type: Optional[Node] + current: Optional[Node] = self while current and not isinstance(current, cls): current = current.parent assert current is None or isinstance(current, cls) @@ -433,9 +433,7 @@ def get_fslocation_from_item( :rtype: A tuple of (str|py.path.local, int) with filename and line number. """ # See Item.location. - location = getattr( - node, "location", None - ) # type: Optional[Tuple[str, Optional[int], str]] + location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None) if location is not None: return location[:2] obj = getattr(node, "obj", None) @@ -560,11 +558,11 @@ def __init__( nodeid: Optional[str] = None, ) -> None: super().__init__(name, parent, config, session, nodeid=nodeid) - self._report_sections = [] # type: List[Tuple[str, str, str]] + self._report_sections: List[Tuple[str, str, str]] = [] #: A list of tuples (name, value) that holds user defined properties #: for this test. - self.user_properties = [] # type: List[Tuple[str, object]] + self.user_properties: List[Tuple[str, object]] = [] def runtest(self) -> None: raise NotImplementedError("runtest must be implemented by Item subclass") diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 8130a441367..f0607cbd849 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -88,8 +88,8 @@ def __init__( class _WithException(Protocol[_F, _ET]): - Exception = None # type: _ET - __call__ = None # type: _F + Exception: _ET + __call__: _F def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]: diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index c206900db95..131873c174a 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -79,9 +79,9 @@ def create_new_paste(contents: Union[str, bytes]) -> str: params = {"code": contents, "lexer": "text", "expiry": "1week"} url = "https://bpaste.net" try: - response = ( + response: str = ( urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8") - ) # type: str + ) except OSError as exc_info: # urllib errors return "bad response: %s" % exc_info m = re.search(r'href="/raw/(\w+)"', response) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 0a4fffed7ae..e66e718f100 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -214,8 +214,8 @@ class HookRecorder: def __init__(self, pluginmanager: PytestPluginManager) -> None: self._pluginmanager = pluginmanager - self.calls = [] # type: List[ParsedCall] - self.ret = None # type: Optional[Union[int, ExitCode]] + self.calls: List[ParsedCall] = [] + self.ret: Optional[Union[int, ExitCode]] = None def before(hook_name: str, hook_impls, kwargs) -> None: self.calls.append(ParsedCall(hook_name, kwargs)) @@ -474,7 +474,7 @@ def __init__( duration: float, ) -> None: try: - self.ret = pytest.ExitCode(ret) # type: Union[int, ExitCode] + self.ret: Union[int, ExitCode] = pytest.ExitCode(ret) """The return value.""" except ValueError: self.ret = ret @@ -626,17 +626,17 @@ class TimeoutExpired(Exception): def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> None: self.request = request - self._mod_collections = ( - WeakKeyDictionary() - ) # type: WeakKeyDictionary[Module, List[Union[Item, Collector]]] + self._mod_collections: WeakKeyDictionary[ + Module, List[Union[Item, Collector]] + ] = (WeakKeyDictionary()) if request.function: - name = request.function.__name__ # type: str + name: str = request.function.__name__ else: name = request.node.name self._name = name self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) - self.plugins = [] # type: List[Union[str, _PluggyPlugin]] + self.plugins: List[Union[str, _PluggyPlugin]] = [] self._cwd_snapshot = CwdSnapshot() self._sys_path_snapshot = SysPathsSnapshot() self._sys_modules_snapshot = self.__take_sys_modules_snapshot() @@ -919,7 +919,7 @@ def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]: test items contained within. """ session = colitems[0].session - result = [] # type: List[Item] + result: List[Item] = [] for colitem in colitems: result.extend(session.genitems(colitem)) return result @@ -1437,7 +1437,7 @@ class LineMatcher: def __init__(self, lines: List[str]) -> None: self.lines = lines - self._log_output = [] # type: List[str] + self._log_output: List[str] = [] def _getlines(self, lines2: Union[str, Sequence[str], Source]) -> Sequence[str]: if isinstance(lines2, str): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index d07615a5a71..07cc4d99cf4 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -197,9 +197,7 @@ def pytest_collect_file( ): return None ihook = parent.session.gethookproxy(path) - module = ihook.pytest_pycollect_makemodule( - path=path, parent=parent - ) # type: Module + module: Module = ihook.pytest_pycollect_makemodule(path=path, parent=parent) return module return None @@ -211,9 +209,9 @@ def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool: def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module": if path.basename == "__init__.py": - pkg = Package.from_parent(parent, fspath=path) # type: Package + pkg: Package = Package.from_parent(parent, fspath=path) return pkg - mod = Module.from_parent(parent, fspath=path) # type: Module + mod: Module = Module.from_parent(parent, fspath=path) return mod @@ -257,9 +255,9 @@ class PyobjMixin: # Function and attributes that the mixin needs (for type-checking only). if TYPE_CHECKING: - name = "" # type: str - parent = None # type: Optional[nodes.Node] - own_markers = [] # type: List[Mark] + name: str = "" + parent: Optional[nodes.Node] = None + own_markers: List[Mark] = [] def getparent(self, cls: Type[nodes._NodeType]) -> Optional[nodes._NodeType]: ... @@ -336,7 +334,7 @@ def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]: file_path = sys.modules[obj.__module__].__file__ if file_path.endswith(".pyc"): file_path = file_path[:-1] - fspath = file_path # type: Union[py.path.local, str] + fspath: Union[py.path.local, str] = file_path lineno = compat_co_firstlineno else: fspath, lineno = getfslineno(obj) @@ -420,8 +418,8 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: dicts = [getattr(self.obj, "__dict__", {})] for basecls in self.obj.__class__.__mro__: dicts.append(basecls.__dict__) - seen = set() # type: Set[str] - values = [] # type: List[Union[nodes.Item, nodes.Collector]] + seen: Set[str] = set() + values: List[Union[nodes.Item, nodes.Collector]] = [] ihook = self.ihook for dic in dicts: # Note: seems like the dict can change during iteration - @@ -696,7 +694,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: init_module, self.config.getini("python_files") ): yield Module.from_parent(self, fspath=init_module) - pkg_prefixes = set() # type: Set[py.path.local] + pkg_prefixes: Set[py.path.local] = set() for direntry in visit(str(this_path), recurse=self._recurse): path = py.path.local(direntry.path) @@ -851,14 +849,14 @@ def newinstance(self): def hasinit(obj: object) -> bool: - init = getattr(obj, "__init__", None) # type: object + init: object = getattr(obj, "__init__", None) if init: return init != object.__init__ return False def hasnew(obj: object) -> bool: - new = getattr(obj, "__new__", None) # type: object + new: object = getattr(obj, "__new__", None) if new: return new != object.__new__ return False @@ -868,13 +866,13 @@ def hasnew(obj: object) -> bool: class CallSpec2: def __init__(self, metafunc: "Metafunc") -> None: self.metafunc = metafunc - self.funcargs = {} # type: Dict[str, object] - self._idlist = [] # type: List[str] - self.params = {} # type: Dict[str, object] + self.funcargs: Dict[str, object] = {} + self._idlist: List[str] = [] + self.params: Dict[str, object] = {} # Used for sorting parametrized resources. - self._arg2scopenum = {} # type: Dict[str, int] - self.marks = [] # type: List[Mark] - self.indices = {} # type: Dict[str, int] + self._arg2scopenum: Dict[str, int] = {} + self.marks: List[Mark] = [] + self.indices: Dict[str, int] = {} def copy(self) -> "CallSpec2": cs = CallSpec2(self.metafunc) @@ -959,7 +957,7 @@ def __init__( #: Class object where the test function is defined in or ``None``. self.cls = cls - self._calls = [] # type: List[CallSpec2] + self._calls: List[CallSpec2] = [] self._arg2fixturedefs = fixtureinfo.name2fixturedefs def parametrize( @@ -1175,9 +1173,9 @@ def _resolve_arg_value_types( * "funcargs" if the argname should be a parameter to the parametrized test function. """ if isinstance(indirect, bool): - valtypes = dict.fromkeys( + valtypes: Dict[str, Literal["params", "funcargs"]] = dict.fromkeys( argnames, "params" if indirect else "funcargs" - ) # type: Dict[str, Literal["params", "funcargs"]] + ) elif isinstance(indirect, Sequence): valtypes = dict.fromkeys(argnames, "funcargs") for arg in indirect: @@ -1296,9 +1294,9 @@ def _idval( msg = prefix + msg.format(argname, idx) raise ValueError(msg) from e elif config: - hook_id = config.hook.pytest_make_parametrize_id( + hook_id: Optional[str] = config.hook.pytest_make_parametrize_id( config=config, val=val, argname=argname - ) # type: Optional[str] + ) if hook_id: return hook_id @@ -1315,7 +1313,7 @@ def _idval( return str(val) elif isinstance(getattr(val, "__name__", None), str): # Name of a class, function, module, etc. - name = getattr(val, "__name__") # type: str + name: str = getattr(val, "__name__") return name return str(argname) + str(idx) @@ -1365,7 +1363,7 @@ def idmaker( test_id_counts = Counter(resolved_ids) # Map the test ID to its next suffix. - test_id_suffixes = defaultdict(int) # type: Dict[str, int] + test_id_suffixes: Dict[str, int] = defaultdict(int) # Suffix non-unique IDs to make them unique. for index, test_id in enumerate(resolved_ids): @@ -1412,7 +1410,7 @@ def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: def write_item(item: nodes.Item) -> None: # Not all items have _fixtureinfo attribute. - info = getattr(item, "_fixtureinfo", None) # type: Optional[FuncFixtureInfo] + info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None) if info is None or not info.name2fixturedefs: # This test item does not use any fixtures. return @@ -1449,7 +1447,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: fm = session._fixturemanager available = [] - seen = set() # type: Set[Tuple[str, str]] + seen: Set[Tuple[str, str]] = set() for argname, fixturedefs in fm._arg2fixturedefs.items(): assert fixturedefs is not None @@ -1590,7 +1588,7 @@ def __init__( fixtureinfo = self.session._fixturemanager.getfixtureinfo( self, self.obj, self.cls, funcargs=True ) - self._fixtureinfo = fixtureinfo # type: FuncFixtureInfo + self._fixtureinfo: FuncFixtureInfo = fixtureinfo self.fixturenames = fixtureinfo.names_closure self._initrequest() @@ -1600,7 +1598,7 @@ def from_parent(cls, parent, **kw): # todo: determine sound type limitations return super().from_parent(parent=parent, **kw) def _initrequest(self) -> None: - self.funcargs = {} # type: Dict[str, object] + self.funcargs: Dict[str, object] = {} self._request = fixtures.FixtureRequest(self) @property diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index fbfe90ed9f9..c4d029c0d5c 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -196,8 +196,8 @@ class ApproxScalar(ApproxBase): # Using Real should be better than this Union, but not possible yet: # https://github.com/python/typeshed/pull/3108 - DEFAULT_ABSOLUTE_TOLERANCE = 1e-12 # type: Union[float, Decimal] - DEFAULT_RELATIVE_TOLERANCE = 1e-6 # type: Union[float, Decimal] + DEFAULT_ABSOLUTE_TOLERANCE: Union[float, Decimal] = 1e-12 + DEFAULT_RELATIVE_TOLERANCE: Union[float, Decimal] = 1e-6 def __repr__(self) -> str: """Return a string communicating both the expected value and the @@ -266,7 +266,7 @@ def __eq__(self, actual) -> bool: return False # Return true if the two numbers are within the tolerance. - result = abs(self.expected - actual) <= self.tolerance # type: bool + result: bool = abs(self.expected - actual) <= self.tolerance return result # Ignore type because of https://github.com/python/mypy/issues/4266. @@ -517,7 +517,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: __tracebackhide__ = True if isinstance(expected, Decimal): - cls = ApproxDecimal # type: Type[ApproxBase] + cls: Type[ApproxBase] = ApproxDecimal elif isinstance(expected, Mapping): cls = ApproxMapping elif _is_numpy_array(expected): @@ -542,7 +542,7 @@ def _is_numpy_array(obj: object) -> bool: """ import sys - np = sys.modules.get("numpy") # type: Any + np: Any = sys.modules.get("numpy") if np is not None: return isinstance(obj, np.ndarray) return False @@ -687,7 +687,7 @@ def raises( __tracebackhide__ = True if isinstance(expected_exception, type): - excepted_exceptions = (expected_exception,) # type: Tuple[Type[_E], ...] + excepted_exceptions: Tuple[Type[_E], ...] = (expected_exception,) else: excepted_exceptions = expected_exception for exc in excepted_exceptions: @@ -699,7 +699,7 @@ def raises( message = f"DID NOT RAISE {expected_exception}" if not args: - match = kwargs.pop("match", None) # type: Optional[Union[str, Pattern[str]]] + match: Optional[Union[str, Pattern[str]]] = kwargs.pop("match", None) if kwargs: msg = "Unexpected keyword arguments passed to pytest.raises: " msg += ", ".join(sorted(kwargs)) @@ -738,7 +738,7 @@ def __init__( self.expected_exception = expected_exception self.message = message self.match_expr = match_expr - self.excinfo = None # type: Optional[_pytest._code.ExceptionInfo[_E]] + self.excinfo: Optional[_pytest._code.ExceptionInfo[_E]] = None def __enter__(self) -> _pytest._code.ExceptionInfo[_E]: self.excinfo = _pytest._code.ExceptionInfo.for_later() diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index cc82a4d7a7f..49f1e590296 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -163,7 +163,7 @@ def __init__(self) -> None: # Type ignored due to the way typeshed handles warnings.catch_warnings. super().__init__(record=True) # type: ignore[call-arg] self._entered = False - self._list = [] # type: List[warnings.WarningMessage] + self._list: List[warnings.WarningMessage] = [] @property def list(self) -> List["warnings.WarningMessage"]: diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 8e7f0f9bba9..58f12517c5b 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -58,13 +58,13 @@ def getworkerinfoline(node): class BaseReport: - when = None # type: Optional[str] - location = None # type: Optional[Tuple[str, Optional[int], str]] - longrepr = ( - None - ) # type: Union[None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr] - sections = [] # type: List[Tuple[str, str]] - nodeid = None # type: str + when: Optional[str] + location: Optional[Tuple[str, Optional[int], str]] + longrepr: Union[ + None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr + ] + sections: List[Tuple[str, str]] + nodeid: str def __init__(self, **kw: Any) -> None: self.__dict__.update(kw) @@ -254,7 +254,7 @@ def __init__( #: A (filesystempath, lineno, domaininfo) tuple indicating the #: actual location of a test item - it might be different from the #: collected one e.g. if a method is inherited from a different module. - self.location = location # type: Tuple[str, Optional[int], str] + self.location: Tuple[str, Optional[int], str] = location #: A name -> value dictionary containing all keywords and #: markers associated with a test invocation. @@ -300,10 +300,14 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": excinfo = call.excinfo sections = [] if not call.excinfo: - outcome = "passed" # type: Literal["passed", "failed", "skipped"] - longrepr = ( - None - ) # type: Union[None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr] + outcome: Literal["passed", "failed", "skipped"] = "passed" + longrepr: Union[ + None, + ExceptionInfo[BaseException], + Tuple[str, int, str], + str, + TerminalRepr, + ] = (None) else: if not isinstance(excinfo, ExceptionInfo): outcome = "failed" @@ -450,11 +454,11 @@ def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]: assert rep.longrepr is not None # TODO: Investigate whether the duck typing is really necessary here. longrepr = cast(ExceptionRepr, rep.longrepr) - result = { + result: Dict[str, Any] = { "reprcrash": serialize_repr_crash(longrepr.reprcrash), "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback), "sections": longrepr.sections, - } # type: Dict[str, Any] + } if isinstance(longrepr, ExceptionChainRepr): result["chain"] = [] for repr_traceback, repr_crash, description in longrepr.chain: @@ -508,13 +512,13 @@ def deserialize_repr_entry(entry_data): if data["reprlocals"]: reprlocals = ReprLocals(data["reprlocals"]["lines"]) - reprentry = ReprEntry( + reprentry: Union[ReprEntry, ReprEntryNative] = ReprEntry( lines=data["lines"], reprfuncargs=reprfuncargs, reprlocals=reprlocals, reprfileloc=reprfileloc, style=data["style"], - ) # type: Union[ReprEntry, ReprEntryNative] + ) elif entry_type == "ReprEntryNative": reprentry = ReprEntryNative(data["lines"]) else: @@ -555,9 +559,9 @@ def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]): description, ) ) - exception_info = ExceptionChainRepr( - chain - ) # type: Union[ExceptionChainRepr,ReprExceptionInfo] + exception_info: Union[ + ExceptionChainRepr, ReprExceptionInfo + ] = ExceptionChainRepr(chain) else: exception_info = ReprExceptionInfo(reprtraceback, reprcrash) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index e617c232640..cce9bdd9713 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -215,7 +215,7 @@ def call_and_report( ) -> TestReport: call = call_runtest_hook(item, when, **kwds) hook = item.ihook - report = hook.pytest_runtest_makereport(item=item, call=call) # type: TestReport + report: TestReport = hook.pytest_runtest_makereport(item=item, call=call) if log: hook.pytest_runtest_logreport(report=report) if check_interactive_exception(call, report): @@ -242,14 +242,14 @@ def call_runtest_hook( item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds ) -> "CallInfo[None]": if when == "setup": - ihook = item.ihook.pytest_runtest_setup # type: Callable[..., None] + ihook: Callable[..., None] = item.ihook.pytest_runtest_setup elif when == "call": ihook = item.ihook.pytest_runtest_call elif when == "teardown": ihook = item.ihook.pytest_runtest_teardown else: assert False, f"Unhandled runtest hook case: {when}" - reraise = (Exit,) # type: Tuple[Type[BaseException], ...] + reraise: Tuple[Type[BaseException], ...] = (Exit,) if not item.config.getoption("usepdb", False): reraise += (KeyboardInterrupt,) return CallInfo.from_call( @@ -309,7 +309,7 @@ def from_call( start = timing.time() precise_start = timing.perf_counter() try: - result = func() # type: Optional[TResult] + result: Optional[TResult] = func() except BaseException: excinfo = ExceptionInfo.from_current() if reraise is not None and isinstance(excinfo.value, reraise): @@ -340,9 +340,9 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: def pytest_make_collect_report(collector: Collector) -> CollectReport: call = CallInfo.from_call(lambda: list(collector.collect()), "collect") - longrepr = None # type: Union[None, Tuple[str, int, str], str, TerminalRepr] + longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None if not call.excinfo: - outcome = "passed" # type: Literal["passed", "skipped", "failed"] + outcome: Literal["passed", "skipped", "failed"] = "passed" else: skip_exceptions = [Skipped] unittest = sys.modules.get("unittest") @@ -373,8 +373,8 @@ class SetupState: """Shared state for setting up/tearing down test items or collectors.""" def __init__(self): - self.stack = [] # type: List[Node] - self._finalizers = {} # type: Dict[Node, List[Callable[[], object]]] + self.stack: List[Node] = [] + self._finalizers: Dict[Node, List[Callable[[], object]]] = {} def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None: """Attach a finalizer to the given colitem.""" @@ -456,7 +456,7 @@ def prepare(self, colitem) -> None: def collect_one_node(collector: Collector) -> CollectReport: ihook = collector.ihook ihook.pytest_collectstart(collector=collector) - rep = ihook.pytest_make_collect_report(collector=collector) # type: CollectReport + rep: CollectReport = ihook.pytest_make_collect_report(collector=collector) call = rep.__dict__.pop("call", None) if call and check_interactive_exception(call, rep): ihook.pytest_exception_interact(node=collector, call=call, report=rep) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 85cbe293151..97eae18fd20 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -35,7 +35,7 @@ class StepwisePlugin: def __init__(self, config: Config) -> None: self.config = config self.active = config.getvalue("stepwise") - self.session = None # type: Optional[Session] + self.session: Optional[Session] = None self.report_status = "" if self.active: diff --git a/src/_pytest/store.py b/src/_pytest/store.py index fbf3c588f36..e5008cfc5a1 100644 --- a/src/_pytest/store.py +++ b/src/_pytest/store.py @@ -83,7 +83,7 @@ class Store: __slots__ = ("_store",) def __init__(self) -> None: - self._store = {} # type: Dict[StoreKey[Any], object] + self._store: Dict[StoreKey[Any], object] = {} def __setitem__(self, key: StoreKey[T], value: T) -> None: """Set a value for key.""" diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index f84797af29e..a5eaeb382da 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -235,7 +235,7 @@ def mywriter(tags, args): def getreportopt(config: Config) -> str: - reportchars = config.option.reportchars # type: str + reportchars: str = config.option.reportchars old_aliases = {"F", "S"} reportopts = "" @@ -267,7 +267,7 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]: elif report.skipped: letter = "s" - outcome = report.outcome # type: str + outcome: str = report.outcome if report.when in ("collect", "setup", "teardown") and outcome == "failed": outcome = "error" letter = "E" @@ -317,27 +317,27 @@ def __init__(self, config: Config, file: Optional[TextIO] = None) -> None: self.config = config self._numcollected = 0 - self._session = None # type: Optional[Session] - self._showfspath = None # type: Optional[bool] + self._session: Optional[Session] = None + self._showfspath: Optional[bool] = None - self.stats = {} # type: Dict[str, List[Any]] - self._main_color = None # type: Optional[str] - self._known_types = None # type: Optional[List[str]] + self.stats: Dict[str, List[Any]] = {} + self._main_color: Optional[str] = None + self._known_types: Optional[List[str]] = None self.startdir = config.invocation_dir self.startpath = config.invocation_params.dir if file is None: file = sys.stdout self._tw = _pytest.config.create_terminal_writer(config, file) self._screen_width = self._tw.fullwidth - self.currentfspath = None # type: Union[None, Path, str, int] + self.currentfspath: Union[None, Path, str, int] = None self.reportchars = getreportopt(config) self.hasmarkup = self._tw.hasmarkup self.isatty = file.isatty() - self._progress_nodeids_reported = set() # type: Set[str] + self._progress_nodeids_reported: Set[str] = set() self._show_progress_info = self._determine_show_progress_info() - self._collect_report_last_write = None # type: Optional[float] - self._already_displayed_warnings = None # type: Optional[int] - self._keyboardinterrupt_memo = None # type: Optional[ExceptionRepr] + self._collect_report_last_write: Optional[float] = None + self._already_displayed_warnings: Optional[int] = None + self._keyboardinterrupt_memo: Optional[ExceptionRepr] = None def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]": """Return whether we should display progress information based on the current config.""" @@ -347,7 +347,7 @@ def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]" # do not show progress if we are showing fixture setup/teardown if self.config.getoption("setupshow", False): return False - cfg = self.config.getini("console_output_style") # type: str + cfg: str = self.config.getini("console_output_style") if cfg == "progress": return "progress" elif cfg == "count": @@ -357,7 +357,7 @@ def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]" @property def verbosity(self) -> int: - verbosity = self.config.option.verbose # type: int + verbosity: int = self.config.option.verbose return verbosity @property @@ -512,9 +512,9 @@ def pytest_runtest_logstart( def pytest_runtest_logreport(self, report: TestReport) -> None: self._tests_ran = True rep = report - res = self.config.hook.pytest_report_teststatus( - report=rep, config=self.config - ) # type: Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]] + res: Tuple[ + str, str, Union[str, Tuple[str, Mapping[str, bool]]] + ] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config) category, letter, word = res if not isinstance(word, tuple): markup = None @@ -718,7 +718,7 @@ def pytest_report_header(self, config: Config) -> List[str]: if config.inipath: line += ", configfile: " + bestrelpath(config.rootpath, config.inipath) - testpaths = config.getini("testpaths") # type: List[str] + testpaths: List[str] = config.getini("testpaths") if config.invocation_params.dir == config.rootpath and config.args == testpaths: line += ", testpaths: {}".format(", ".join(testpaths)) @@ -755,7 +755,7 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: # because later versions are going to get rid of them anyway. if self.config.option.verbose < 0: if self.config.option.verbose < -1: - counts = {} # type: Dict[str, int] + counts: Dict[str, int] = {} for item in items: name = item.nodeid.split("::", 1)[0] counts[name] = counts.get(name, 0) + 1 @@ -765,7 +765,7 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: for item in items: self._tw.line(item.nodeid) return - stack = [] # type: List[Node] + stack: List[Node] = [] indent = "" for item in items: needed_collectors = item.listchain()[1:] # strip root node @@ -896,9 +896,7 @@ def getreports(self, name: str): def summary_warnings(self) -> None: if self.hasopt("w"): - all_warnings = self.stats.get( - "warnings" - ) # type: Optional[List[WarningReport]] + all_warnings: Optional[List[WarningReport]] = self.stats.get("warnings") if not all_warnings: return @@ -911,9 +909,9 @@ def summary_warnings(self) -> None: if not warning_reports: return - reports_grouped_by_message = ( + reports_grouped_by_message: Dict[str, List[WarningReport]] = ( order_preserving_dict() - ) # type: Dict[str, List[WarningReport]] + ) for wr in warning_reports: reports_grouped_by_message.setdefault(wr.message, []).append(wr) @@ -927,7 +925,7 @@ def collapsed_location_report(reports: List[WarningReport]) -> str: if len(locations) < 10: return "\n".join(map(str, locations)) - counts_by_filename = order_preserving_dict() # type: Dict[str, int] + counts_by_filename: Dict[str, int] = order_preserving_dict() for loc in locations: key = str(loc).split("::", 1)[0] counts_by_filename[key] = counts_by_filename.get(key, 0) + 1 @@ -954,7 +952,7 @@ def collapsed_location_report(reports: List[WarningReport]) -> str: def summary_passes(self) -> None: if self.config.option.tbstyle != "no": if self.hasopt("P"): - reports = self.getreports("passed") # type: List[TestReport] + reports: List[TestReport] = self.getreports("passed") if not reports: return self.write_sep("=", "PASSES") @@ -992,7 +990,7 @@ def print_teardown_sections(self, rep: TestReport) -> None: def summary_failures(self) -> None: if self.config.option.tbstyle != "no": - reports = self.getreports("failed") # type: List[BaseReport] + reports: List[BaseReport] = self.getreports("failed") if not reports: return self.write_sep("=", "FAILURES") @@ -1009,7 +1007,7 @@ def summary_failures(self) -> None: def summary_errors(self) -> None: if self.config.option.tbstyle != "no": - reports = self.getreports("error") # type: List[BaseReport] + reports: List[BaseReport] = self.getreports("error") if not reports: return self.write_sep("=", "ERRORS") @@ -1105,7 +1103,7 @@ def show_xpassed(lines: List[str]) -> None: lines.append(f"{verbose_word} {pos} {reason}") def show_skipped(lines: List[str]) -> None: - skipped = self.stats.get("skipped", []) # type: List[CollectReport] + skipped: List[CollectReport] = self.stats.get("skipped", []) fskips = _folded_skips(self.startpath, skipped) if skipped else [] if not fskips: return @@ -1121,16 +1119,16 @@ def show_skipped(lines: List[str]) -> None: else: lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason)) - REPORTCHAR_ACTIONS = { + REPORTCHAR_ACTIONS: Mapping[str, Callable[[List[str]], None]] = { "x": show_xfailed, "X": show_xpassed, "f": partial(show_simple, "failed"), "s": show_skipped, "p": partial(show_simple, "passed"), "E": partial(show_simple, "error"), - } # type: Mapping[str, Callable[[List[str]], None]] + } - lines = [] # type: List[str] + lines: List[str] = [] for char in self.reportchars: action = REPORTCHAR_ACTIONS.get(char) if action: # skipping e.g. "P" (passed with output) here. @@ -1161,7 +1159,7 @@ def _determine_main_color(self, unknown_type_seen: bool) -> str: return main_color def _set_main_color(self) -> None: - unknown_types = [] # type: List[str] + unknown_types: List[str] = [] for found_type in self.stats.keys(): if found_type: # setup/teardown reports have an empty key, ignore them if found_type not in KNOWN_TYPES and found_type not in unknown_types: @@ -1236,7 +1234,7 @@ def _get_line_with_reprcrash_message( def _folded_skips( startpath: Path, skipped: Sequence[CollectReport], ) -> List[Tuple[int, str, Optional[int], str]]: - d = {} # type: Dict[Tuple[str, Optional[int], str], List[CollectReport]] + d: Dict[Tuple[str, Optional[int], str], List[CollectReport]] = {} for event in skipped: assert event.longrepr is not None assert isinstance(event.longrepr, tuple), (event, event.longrepr) @@ -1253,11 +1251,11 @@ def _folded_skips( and "skip" in keywords and "pytestmark" not in keywords ): - key = (fspath, None, reason) # type: Tuple[str, Optional[int], str] + key: Tuple[str, Optional[int], str] = (fspath, None, reason) else: key = (fspath, lineno, reason) d.setdefault(key, []).append(event) - values = [] # type: List[Tuple[int, str, Optional[int], str]] + values: List[Tuple[int, str, Optional[int], str]] = [] for key, events in d.items(): values.append((len(events), *key)) return values @@ -1286,7 +1284,7 @@ def _make_plural(count: int, noun: str) -> Tuple[int, str]: def _plugin_nameversions(plugininfo) -> List[str]: - values = [] # type: List[str] + values: List[str] = [] for plugin, dist in plugininfo: # Gets us name and version! name = "{dist.project_name}-{dist.version}".format(dist=dist) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 62c6e90b71b..6dc404a3949 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -55,7 +55,7 @@ def pytest_pycollect_makeitem( except Exception: return None # Yes, so let's collect it. - item = UnitTestCase.from_parent(collector, name=name, obj=obj) # type: UnitTestCase + item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj) return item @@ -141,12 +141,12 @@ def fixture(self, request: FixtureRequest) -> Generator[None, None, None]: class TestCaseFunction(Function): nofuncargs = True - _excinfo = None # type: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] - _testcase = None # type: Optional[unittest.TestCase] + _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None + _testcase: Optional["unittest.TestCase"] = None def setup(self) -> None: # A bound method to be called during teardown() if set (see 'runtest()'). - self._explicit_tearDown = None # type: Optional[Callable[[], None]] + self._explicit_tearDown: Optional[Callable[[], None]] = None assert self.parent is not None self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined] self._obj = getattr(self._testcase, self.name) @@ -320,7 +320,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: @hookimpl(hookwrapper=True) def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: - ut = sys.modules["twisted.python.failure"] # type: Any + ut: Any = sys.modules["twisted.python.failure"] Failure__init__ = ut.Failure.__init__ check_testcase_implements_trial_reporter() diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index d1ed43300db..5f0ade4105c 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -778,7 +778,7 @@ def entry(): ) excinfo = pytest.raises(ValueError, mod.entry) - styles = ("long", "short") # type: Tuple[_TracebackStyle, ...] + styles: Tuple[_TracebackStyle, ...] = ("long", "short") for style in styles: p = FormattedExcinfo(style=style) reprtb = p.repr_traceback(excinfo) @@ -905,7 +905,7 @@ def entry(): ) excinfo = pytest.raises(ValueError, mod.entry) - styles = ("short", "long", "no") # type: Tuple[_TracebackStyle, ...] + styles: Tuple[_TracebackStyle, ...] = ("short", "long", "no") for style in styles: for showlocals in (True, False): repr = excinfo.getrepr(style=style, showlocals=showlocals) @@ -1370,7 +1370,7 @@ def f(): @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"]) def test_repr_traceback_with_unicode(style, encoding): if encoding is None: - msg = "☹" # type: Union[str, bytes] + msg: Union[str, bytes] = "☹" else: msg = "☹".encode(encoding) try: diff --git a/testing/code/test_source.py b/testing/code/test_source.py index d12c55d935b..e259e04cfef 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -337,7 +337,7 @@ def test_findsource(monkeypatch) -> None: assert src is not None assert "if 1:" in str(src) - d = {} # type: Dict[str, Any] + d: Dict[str, Any] = {} eval(co, d) src, lineno = findsource(d["x"]) assert src is not None diff --git a/testing/example_scripts/issue_519.py b/testing/example_scripts/issue_519.py index 021dada4923..3928294886f 100644 --- a/testing/example_scripts/issue_519.py +++ b/testing/example_scripts/issue_519.py @@ -15,7 +15,7 @@ def pytest_generate_tests(metafunc): @pytest.fixture(scope="session") def checked_order(): - order = [] # type: List[Tuple[str, str, str]] + order: List[Tuple[str, str, str]] = [] yield order pprint.pprint(order) diff --git a/testing/example_scripts/unittest/test_unittest_asyncio.py b/testing/example_scripts/unittest/test_unittest_asyncio.py index 21b9d2cd963..bbc752de5c1 100644 --- a/testing/example_scripts/unittest/test_unittest_asyncio.py +++ b/testing/example_scripts/unittest/test_unittest_asyncio.py @@ -2,7 +2,7 @@ from unittest import IsolatedAsyncioTestCase # type: ignore -teardowns = [] # type: List[None] +teardowns: List[None] = [] class AsyncArguments(IsolatedAsyncioTestCase): diff --git a/testing/example_scripts/unittest/test_unittest_asynctest.py b/testing/example_scripts/unittest/test_unittest_asynctest.py index 47b5f3f6d63..fb26617067c 100644 --- a/testing/example_scripts/unittest/test_unittest_asynctest.py +++ b/testing/example_scripts/unittest/test_unittest_asynctest.py @@ -5,7 +5,7 @@ import asynctest -teardowns = [] # type: List[None] +teardowns: List[None] = [] class Test(asynctest.TestCase): diff --git a/testing/logging/test_formatter.py b/testing/logging/test_formatter.py index a90384a9553..335166caa2f 100644 --- a/testing/logging/test_formatter.py +++ b/testing/logging/test_formatter.py @@ -41,7 +41,7 @@ def test_multiline_message() -> None: logfmt = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s" - record = logging.LogRecord( + record: Any = logging.LogRecord( name="dummy", level=logging.INFO, pathname="dummypath", @@ -49,7 +49,7 @@ def test_multiline_message() -> None: msg="Test Message line1\nline2", args=(), exc_info=None, - ) # type: Any + ) # this is called by logging.Formatter.format record.message = record.getMessage() diff --git a/testing/python/collect.py b/testing/python/collect.py index 01294039860..4d5f4c6895f 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1040,7 +1040,7 @@ def test_filter_traceback_generated_code(self) -> None: from _pytest._code import filter_traceback try: - ns = {} # type: Dict[str, Any] + ns: Dict[str, Any] = {} exec("def foo(): raise ValueError", ns) ns["foo"]() except ValueError: diff --git a/testing/python/integration.py b/testing/python/integration.py index 854593a65c0..f006e5ed4ee 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -33,7 +33,7 @@ class MyClass(object): # this hook finds funcarg factories rep = runner.collect_one_node(collector=modcol) # TODO: Don't treat as Any. - clscol = rep.result[0] # type: Any + clscol: Any = rep.result[0] clscol.obj = lambda arg1: None clscol.funcargs = {} pytest._fillfuncargs(clscol) @@ -67,7 +67,7 @@ class MyClass(object): # this hook finds funcarg factories rep = runner.collect_one_node(modcol) # TODO: Don't treat as Any. - clscol = rep.result[0] # type: Any + clscol: Any = rep.result[0] clscol.obj = lambda: None clscol.funcargs = {} pytest._fillfuncargs(clscol) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 6b59104567a..2a6b3dc5414 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -45,8 +45,8 @@ class DefinitionMock(python.FunctionDefinition): _nodeid = attr.ib() names = getfuncargnames(func) - fixtureinfo = FuncFixtureInfoMock(names) # type: Any - definition = DefinitionMock._create(func, "mock::nodeid") # type: Any + fixtureinfo: Any = FuncFixtureInfoMock(names) + definition: Any = DefinitionMock._create(func, "mock::nodeid") return python.Metafunc(definition, fixtureinfo, config) def test_no_funcargs(self) -> None: @@ -326,10 +326,10 @@ def getini(self, name): option = "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" - values = [ + values: List[Tuple[str, Any, str]] = [ ("ação", MockConfig({option: True}), "ação"), ("ação", MockConfig({option: False}), "a\\xe7\\xe3o"), - ] # type: List[Tuple[str, Any, str]] + ] for val, config, expected in values: actual = _idval(val, "a", 6, None, nodeid=None, config=config) assert actual == expected @@ -508,10 +508,10 @@ def getini(self, name): option = "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" - values = [ + values: List[Tuple[Any, str]] = [ (MockConfig({option: True}), "ação"), (MockConfig({option: False}), "a\\xe7\\xe3o"), - ] # type: List[Tuple[Any, str]] + ] for config, expected in values: result = idmaker( ("a",), [pytest.param("string")], idfn=lambda _: "ação", config=config, @@ -540,10 +540,10 @@ def getini(self, name): option = "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" - values = [ + values: List[Tuple[Any, str]] = [ (MockConfig({option: True}), "ação"), (MockConfig({option: False}), "a\\xe7\\xe3o"), - ] # type: List[Tuple[Any, str]] + ] for config, expected in values: result = idmaker( ("a",), [pytest.param("string")], ids=["ação"], config=config, @@ -1519,9 +1519,9 @@ def test_parametrize_some_arguments_auto_scope( self, testdir: Testdir, monkeypatch ) -> None: """Integration test for (#3941)""" - class_fix_setup = [] # type: List[object] + class_fix_setup: List[object] = [] monkeypatch.setattr(sys, "class_fix_setup", class_fix_setup, raising=False) - func_fix_setup = [] # type: List[object] + func_fix_setup: List[object] = [] monkeypatch.setattr(sys, "func_fix_setup", func_fix_setup, raising=False) testdir.makepyfile( diff --git a/testing/test_assertion.py b/testing/test_assertion.py index f91bc3cb411..e3b6fa51906 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1068,7 +1068,7 @@ class TestTruncateExplanation: LINES_IN_TRUNCATION_MSG = 2 def test_doesnt_truncate_when_input_is_empty_list(self) -> None: - expl = [] # type: List[str] + expl: List[str] = [] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result == expl diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 7dcaf10ea1a..9f0aa31d132 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -45,7 +45,7 @@ def getmsg( src = "\n".join(_pytest._code.Code(f).source().lines) mod = rewrite(src) code = compile(mod, "", "exec") - ns = {} # type: Dict[str, object] + ns: Dict[str, object] = {} if extra_ns is not None: ns.update(extra_ns) exec(code, ns) @@ -1242,8 +1242,8 @@ def hook(self, pytestconfig, monkeypatch, testdir) -> AssertionRewritingHook: """ import importlib.machinery - self.find_spec_calls = [] # type: List[str] - self.initial_paths = set() # type: Set[py.path.local] + self.find_spec_calls: List[str] = [] + self.initial_paths: Set[py.path.local] = set() class StubSession: _initialpaths = self.initial_paths diff --git a/testing/test_compat.py b/testing/test_compat.py index 752e2d0e14d..5239b92c74b 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -211,7 +211,7 @@ def prop(self) -> int: def test_assert_never_union() -> None: - x = 10 # type: Union[int, str] + x: Union[int, str] = 10 if isinstance(x, int): pass @@ -229,7 +229,7 @@ def test_assert_never_union() -> None: def test_assert_never_enum() -> None: E = enum.Enum("E", "a b") - x = E.a # type: E + x: E = E.a if x is E.a: pass @@ -246,7 +246,7 @@ def test_assert_never_enum() -> None: def test_assert_never_literal() -> None: - x = "a" # type: Literal["a", "b"] + x: Literal["a", "b"] = "a" if x == "a": pass diff --git a/testing/test_config.py b/testing/test_config.py index 39f88d9450d..582d241a6b7 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -507,7 +507,7 @@ def test_absolute_win32_path(self, testdir): class TestConfigAPI: def test_config_trace(self, testdir) -> None: config = testdir.parseconfig() - values = [] # type: List[str] + values: List[str] = [] config.trace.root.setwriter(values.append) config.trace("hello") assert len(values) == 1 @@ -809,7 +809,7 @@ def test_basic_behavior(self, _sys_snapshot): def test_invocation_params_args(self, _sys_snapshot) -> None: """Show that fromdictargs can handle args in their "orig" format""" - option_dict = {} # type: Dict[str, object] + option_dict: Dict[str, object] = {} args = ["-vvvv", "-s", "a", "b"] config = Config.fromdictargs(option_dict, args) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index ef4ff6a7750..006bea96280 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -867,7 +867,7 @@ def test_mangle_test_address(): def test_dont_configure_on_workers(tmpdir) -> None: - gotten = [] # type: List[object] + gotten: List[object] = [] class FakeConfig: if TYPE_CHECKING: @@ -1102,9 +1102,10 @@ def test_unicode_issue368(testdir) -> None: class Report(BaseReport): longrepr = ustr - sections = [] # type: List[Tuple[str, str]] + sections: List[Tuple[str, str]] = [] nodeid = "something" location = "tests/filename.py", 42, "TestClass.method" + when = "teardown" test_report = cast(TestReport, Report()) @@ -1372,7 +1373,7 @@ def test_global_properties(testdir, xunit_family) -> None: log = LogXML(str(path), None, family=xunit_family) class Report(BaseReport): - sections = [] # type: List[Tuple[str, str]] + sections: List[Tuple[str, str]] = [] nodeid = "test_node_id" log.pytest_sessionstart() @@ -1408,7 +1409,7 @@ def test_url_property(testdir) -> None: class Report(BaseReport): longrepr = "FooBarBaz" - sections = [] # type: List[Tuple[str, str]] + sections: List[Tuple[str, str]] = [] nodeid = "something" location = "tests/filename.py", 42, "TestClass.method" url = test_url diff --git a/testing/test_meta.py b/testing/test_meta.py index 25a46a35f05..9201bd21611 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -13,7 +13,7 @@ def _modules() -> List[str]: - pytest_pkg = _pytest.__path__ # type: str # type: ignore + pytest_pkg: str = _pytest.__path__ # type: ignore return sorted( n for _, n, _ in pkgutil.walk_packages(pytest_pkg, prefix=_pytest.__name__ + ".") diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index f149e069561..73fe313e5c9 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -130,7 +130,7 @@ def test_setitem() -> None: def test_setitem_deleted_meanwhile() -> None: - d = {} # type: Dict[str, object] + d: Dict[str, object] = {} monkeypatch = MonkeyPatch() monkeypatch.setitem(d, "x", 2) del d["x"] @@ -155,7 +155,7 @@ def test_setenv_deleted_meanwhile(before: bool) -> None: def test_delitem() -> None: - d = {"x": 1} # type: Dict[str, object] + d: Dict[str, object] = {"x": 1} monkeypatch = MonkeyPatch() monkeypatch.delitem(d, "x") assert "x" not in d diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 7f88a13eb43..2a96f29a12c 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -7,7 +7,7 @@ class TestPasteCapture: @pytest.fixture def pastebinlist(self, monkeypatch, request) -> List[Union[str, bytes]]: - pastebinlist = [] # type: List[Union[str, bytes]] + pastebinlist: List[Union[str, bytes]] = [] plugin = request.config.pluginmanager.getplugin("pastebin") monkeypatch.setattr(plugin, "create_new_paste", pastebinlist.append) return pastebinlist diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index a083f4b4f37..2099f5ae1e3 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -100,7 +100,7 @@ def pytest_plugin_registered(self): saveindent.append(pytestpm.trace.root.indent) raise ValueError() - values = [] # type: List[str] + values: List[str] = [] pytestpm.trace.root.setwriter(values.append) undo = pytestpm.enable_tracing() try: diff --git a/testing/test_pytester.py b/testing/test_pytester.py index d27000f3b17..dd3855c69ff 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -227,7 +227,7 @@ def test_inline_run_test_module_not_cleaned_up(self, testdir) -> None: def spy_factory(self): class SysModulesSnapshotSpy: - instances = [] # type: List[SysModulesSnapshotSpy] + instances: List["SysModulesSnapshotSpy"] = [] # noqa: F821 def __init__(self, preserve=None) -> None: SysModulesSnapshotSpy.instances.append(self) @@ -408,7 +408,7 @@ def test_preserve_container(self, monkeypatch, path_type) -> None: original_data = list(getattr(sys, path_type)) original_other = getattr(sys, other_path_type) original_other_data = list(original_other) - new = [] # type: List[object] + new: List[object] = [] snapshot = SysPathsSnapshot() monkeypatch.setattr(sys, path_type, new) snapshot.restore() diff --git a/testing/test_reports.py b/testing/test_reports.py index 67ace3943d6..d18e680b775 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -286,9 +286,9 @@ def test_a(): reprec = testdir.inline_run() if report_class is TestReport: - reports = reprec.getreports( - "pytest_runtest_logreport" - ) # type: Union[Sequence[TestReport], Sequence[CollectReport]] + reports: Union[ + Sequence[TestReport], Sequence[CollectReport] + ] = reprec.getreports("pytest_runtest_logreport") # we have 3 reports: setup/call/teardown assert len(reports) == 3 # get the call report diff --git a/testing/test_runner.py b/testing/test_runner.py index 0101f68233b..95b8f5fccba 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -444,11 +444,11 @@ class TestClass(object): assert res[1].name == "TestClass" -reporttypes = [ +reporttypes: List[Type[reports.BaseReport]] = [ reports.BaseReport, reports.TestReport, reports.CollectReport, -] # type: List[Type[reports.BaseReport]] +] @pytest.mark.parametrize( @@ -456,7 +456,7 @@ class TestClass(object): ) def test_report_extra_parameters(reporttype: Type[reports.BaseReport]) -> None: args = list(inspect.signature(reporttype.__init__).parameters.keys())[1:] - basekw = dict.fromkeys(args, []) # type: Dict[str, List[object]] + basekw: Dict[str, List[object]] = dict.fromkeys(args, []) report = reporttype(newthing=1, **basekw) assert report.newthing == 1 @@ -898,7 +898,7 @@ def runtest(self): def test_current_test_env_var(testdir, monkeypatch) -> None: - pytest_current_test_vars = [] # type: List[Tuple[str, str]] + pytest_current_test_vars: List[Tuple[str, str]] = [] monkeypatch.setattr( sys, "pytest_current_test_vars", pytest_current_test_vars, raising=False ) diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index 1abb35043b7..ef65a24cd79 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -246,7 +246,7 @@ def test_setup_teardown_function_level_with_optional_argument( """Parameter to setup/teardown xunit-style functions parameter is now optional (#1728).""" import sys - trace_setups_teardowns = [] # type: List[str] + trace_setups_teardowns: List[str] = [] monkeypatch.setattr( sys, "trace_setups_teardowns", trace_setups_teardowns, raising=False ) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index d4c21c98537..bd6e7b968b0 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -270,7 +270,7 @@ def test_cleanup_lock_create(self, tmp_path): def test_lock_register_cleanup_removal(self, tmp_path: Path) -> None: lock = create_cleanup_lock(tmp_path) - registry = [] # type: List[Callable[..., None]] + registry: List[Callable[..., None]] = [] register_cleanup_lock_removal(lock, register=registry.append) (cleanup_func,) = registry diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 1aa885264af..f6c8c48eddc 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1162,7 +1162,7 @@ def test_pdb_teardown_called(testdir, monkeypatch) -> None: We delay the normal tearDown() calls when --pdb is given, so this ensures we are calling tearDown() eventually to avoid memory leaks when using --pdb. """ - teardowns = [] # type: List[str] + teardowns: List[str] = [] monkeypatch.setattr( pytest, "test_pdb_teardown_called_teardowns", teardowns, raising=False ) @@ -1194,7 +1194,7 @@ def test_2(self): @pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"]) def test_pdb_teardown_skipped(testdir, monkeypatch, mark: str) -> None: """With --pdb, setUp and tearDown should not be called for skipped tests.""" - tracked = [] # type: List[str] + tracked: List[str] = [] monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False) testdir.makepyfile( diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 3af253ecda2..66898041f08 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -656,9 +656,9 @@ class TestStackLevel: @pytest.fixture def capwarn(self, testdir): class CapturedWarnings: - captured = ( - [] - ) # type: List[Tuple[warnings.WarningMessage, Optional[Tuple[str, int, str]]]] + captured: List[ + Tuple[warnings.WarningMessage, Optional[Tuple[str, int, str]]] + ] = ([]) @classmethod def pytest_warning_recorded(cls, warning_message, when, nodeid, location): From b6b75383ceae9184e82fe6df884459f99ecaba9b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 5 Oct 2020 18:30:40 -0700 Subject: [PATCH 679/823] py36+: remove _pytest.compat.order_preserving_dict --- src/_pytest/cacheprovider.py | 5 ++--- src/_pytest/compat.py | 12 ------------ src/_pytest/fixtures.py | 20 +++++--------------- src/_pytest/terminal.py | 7 ++----- 4 files changed, 9 insertions(+), 35 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 9d548880e34..2c8128f6175 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -22,7 +22,6 @@ from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.compat import final -from _pytest.compat import order_preserving_dict from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser @@ -367,8 +366,8 @@ def pytest_collection_modifyitems( yield if self.active: - new_items: Dict[str, nodes.Item] = order_preserving_dict() - other_items: Dict[str, nodes.Item] = order_preserving_dict() + new_items: Dict[str, nodes.Item] = {} + other_items: Dict[str, nodes.Item] = {} for item in items: if item.nodeid not in self.cached_nodeids: new_items[item.nodeid] = item diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index f704a990e33..0b6b1ca074c 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -366,18 +366,6 @@ def __get__(self, instance, owner=None): return value -# Sometimes an algorithm needs a dict which yields items in the order in which -# they were inserted when iterated. Since Python 3.7, `dict` preserves -# insertion order. Since `dict` is faster and uses less memory than -# `OrderedDict`, prefer to use it if possible. -if sys.version_info >= (3, 7): - order_preserving_dict = dict -else: - from collections import OrderedDict - - order_preserving_dict = OrderedDict - - # Perform exhaustiveness checking. # # Consider this example: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f00f534d8df..cd114456da6 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -45,7 +45,6 @@ from _pytest.compat import getlocation from _pytest.compat import is_generator from _pytest.compat import NOTSET -from _pytest.compat import order_preserving_dict from _pytest.compat import safe_getattr from _pytest.config import _PluggyPlugin from _pytest.config import Config @@ -276,21 +275,12 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]: item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque) items_by_argkey[scopenum] = item_d for item in items: - # cast is a workaround for https://github.com/python/typeshed/issues/3800. - keys = cast( - "Dict[_Key, None]", - order_preserving_dict.fromkeys( - get_parametrized_fixture_keys(item, scopenum), None - ), - ) + keys = dict.fromkeys(get_parametrized_fixture_keys(item, scopenum), None) if keys: d[item] = keys for key in keys: item_d[key].append(item) - # cast is a workaround for https://github.com/python/typeshed/issues/3800. - items_dict = cast( - Dict[nodes.Item, None], order_preserving_dict.fromkeys(items, None) - ) + items_dict = dict.fromkeys(items, None) return list(reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, 0)) @@ -314,17 +304,17 @@ def reorder_items_atscope( return items ignore: Set[Optional[_Key]] = set() items_deque = deque(items) - items_done: Dict[nodes.Item, None] = order_preserving_dict() + items_done: Dict[nodes.Item, None] = {} scoped_items_by_argkey = items_by_argkey[scopenum] scoped_argkeys_cache = argkeys_cache[scopenum] while items_deque: - no_argkey_group: Dict[nodes.Item, None] = order_preserving_dict() + no_argkey_group: Dict[nodes.Item, None] = {} slicing_argkey = None while items_deque: item = items_deque.popleft() if item in items_done or item in no_argkey_group: continue - argkeys = order_preserving_dict.fromkeys( + argkeys = dict.fromkeys( (k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None ) if not argkeys: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index a5eaeb382da..0e8db20c760 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -35,7 +35,6 @@ from _pytest._code.code import ExceptionRepr from _pytest._io.wcwidth import wcswidth from _pytest.compat import final -from _pytest.compat import order_preserving_dict from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode @@ -909,9 +908,7 @@ def summary_warnings(self) -> None: if not warning_reports: return - reports_grouped_by_message: Dict[str, List[WarningReport]] = ( - order_preserving_dict() - ) + reports_grouped_by_message: Dict[str, List[WarningReport]] = {} for wr in warning_reports: reports_grouped_by_message.setdefault(wr.message, []).append(wr) @@ -925,7 +922,7 @@ def collapsed_location_report(reports: List[WarningReport]) -> str: if len(locations) < 10: return "\n".join(map(str, locations)) - counts_by_filename: Dict[str, int] = order_preserving_dict() + counts_by_filename: Dict[str, int] = {} for loc in locations: key = str(loc).split("::", 1)[0] counts_by_filename[key] = counts_by_filename.get(key, 0) + 1 From 13ddec9a006d53bd5144459b27f367e03f8fb377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Mari=C3=B1ez?= Date: Tue, 6 Oct 2020 10:48:34 -0400 Subject: [PATCH 680/823] Add alias clarification to deprecation warning (#7829) Co-authored-by: Bruno Oliveira --- changelog/7815.doc.rst | 1 + src/_pytest/deprecated.py | 7 ++++--- src/_pytest/fixtures.py | 17 +++++++++++++++-- src/pytest/__init__.py | 2 +- testing/deprecated_test.py | 19 ++++++++++++++++++- testing/python/fixtures.py | 2 +- 6 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 changelog/7815.doc.rst diff --git a/changelog/7815.doc.rst b/changelog/7815.doc.rst new file mode 100644 index 00000000000..d799bb42500 --- /dev/null +++ b/changelog/7815.doc.rst @@ -0,0 +1 @@ +Improve deprecation warning message for ``pytest._fillfuncargs()``. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index ecdb60d37f5..fd00fe2d6d5 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -20,9 +20,10 @@ } -FILLFUNCARGS = PytestDeprecationWarning( - "The `_fillfuncargs` function is deprecated, use " - "function._request._fillfixtures() instead if you cannot avoid reaching into internals." +FILLFUNCARGS = UnformattedWarning( + PytestDeprecationWarning, + "{name} is deprecated, use " + "function._request._fillfixtures() instead if you cannot avoid reaching into internals.", ) PYTEST_COLLECT_MODULE = UnformattedWarning( diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index f00f534d8df..a407f626de9 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -350,9 +350,22 @@ def reorder_items_atscope( return items_done +def _fillfuncargs(function: "Function") -> None: + """Fill missing fixtures for a test function, old public API (deprecated).""" + warnings.warn(FILLFUNCARGS.format(name="pytest._fillfuncargs()"), stacklevel=2) + _fill_fixtures_impl(function) + + def fillfixtures(function: "Function") -> None: - """Fill missing funcargs for a test function.""" - warnings.warn(FILLFUNCARGS, stacklevel=2) + """Fill missing fixtures for a test function (deprecated).""" + warnings.warn( + FILLFUNCARGS.format(name="_pytest.fixtures.fillfixtures()"), stacklevel=2 + ) + _fill_fixtures_impl(function) + + +def _fill_fixtures_impl(function: "Function") -> None: + """Internal implementation to fill fixtures on the given function object.""" try: request = function._request except AttributeError: diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index c4c28191877..a9c1ee0282b 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -11,7 +11,7 @@ from _pytest.config import main from _pytest.config import UsageError from _pytest.debugging import pytestPDB as __pytestPDB -from _pytest.fixtures import fillfixtures as _fillfuncargs +from _pytest.fixtures import _fillfuncargs from _pytest.fixtures import fixture from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import yield_fixture diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index eb5d527f52b..5fe9ad7305f 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,3 +1,4 @@ +import re import warnings from unittest import mock @@ -26,11 +27,27 @@ def test_external_plugins_integrated(testdir, plugin): def test_fillfuncargs_is_deprecated() -> None: with pytest.warns( pytest.PytestDeprecationWarning, - match="The `_fillfuncargs` function is deprecated", + match=re.escape( + "pytest._fillfuncargs() is deprecated, use " + "function._request._fillfixtures() instead if you cannot avoid reaching into internals." + ), ): pytest._fillfuncargs(mock.Mock()) +def test_fillfixtures_is_deprecated() -> None: + import _pytest.fixtures + + with pytest.warns( + pytest.PytestDeprecationWarning, + match=re.escape( + "_pytest.fixtures.fillfixtures() is deprecated, use " + "function._request._fillfixtures() instead if you cannot avoid reaching into internals." + ), + ): + _pytest.fixtures.fillfixtures(mock.Mock()) + + def test_minus_k_dash_is_deprecated(testdir) -> None: threepass = testdir.makepyfile( test_threepass=""" diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index c8b200fcca3..bd82125e7d0 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -87,7 +87,7 @@ class T: class TestFillFixtures: def test_fillfuncargs_exposed(self): # used by oejskit, kept for compatibility - assert pytest._fillfuncargs == fixtures.fillfixtures + assert pytest._fillfuncargs == fixtures._fillfuncargs def test_funcarg_lookupfails(self, testdir): testdir.copy_example() From af3759a50372602be748b126b3390ed38c072713 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 7 Oct 2020 17:21:55 -0400 Subject: [PATCH 681/823] Parser.addini() can take and defaults to 'string' --- src/_pytest/config/argparsing.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 86e9c990aed..9a481965526 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -160,20 +160,23 @@ def addini( self, name: str, help: str, - type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None, + type: Optional[ + "Literal['string', 'pathlist', 'args', 'linelist', 'bool']" + ] = None, default=None, ) -> None: """Register an ini-file option. :name: Name of the ini-variable. - :type: Type of the variable, can be ``pathlist``, ``args``, ``linelist`` - or ``bool``. + :type: Type of the variable, can be ``string``, ``pathlist``, ``args``, + ``linelist`` or ``bool``. Defaults to ``string`` if ``None`` or + not passed. :default: Default value if no ini-file option exists but is queried. The value of ini-variables can be retrieved via a call to :py:func:`config.getini(name) <_pytest.config.Config.getini>`. """ - assert type in (None, "pathlist", "args", "linelist", "bool") + assert type in (None, "string", "pathlist", "args", "linelist", "bool") self._inidict[name] = (help, type, default) self._ininames.append(name) From 76acb4433098c069b21903dec18ad1df5696b4fc Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 7 Oct 2020 17:56:54 -0400 Subject: [PATCH 682/823] Update tests to cover explicit None and "string" as addini() types --- src/_pytest/config/__init__.py | 2 +- testing/test_config.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 39fbe01a064..7e486e99e50 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1402,7 +1402,7 @@ def _getini(self, name: str): elif type == "bool": return _strtobool(str(value).strip()) else: - assert type is None + assert type in [None, "string"] return value def _getconftest_pathlist( diff --git a/testing/test_config.py b/testing/test_config.py index 582d241a6b7..f3fa6437239 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -570,11 +570,17 @@ def test_getconftest_pathlist(self, testdir, tmpdir): assert pl[0] == tmpdir assert pl[1] == somepath - def test_addini(self, testdir): + @pytest.mark.parametrize("maybe_type", ["not passed", "None", '"string"']) + def test_addini(self, testdir, maybe_type): + if maybe_type == "not passed": + type_string = "" + else: + type_string = f", {maybe_type}" + testdir.makeconftest( - """ + f""" def pytest_addoption(parser): - parser.addini("myname", "my new ini value") + parser.addini("myname", "my new ini value"{type_string}) """ ) testdir.makeini( From 1630c3726618b7654a8134ecec6247c18c8bb784 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 7 Oct 2020 18:06:13 -0400 Subject: [PATCH 683/823] Added changelog/7872.doc.rst --- changelog/7872.doc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/7872.doc.rst diff --git a/changelog/7872.doc.rst b/changelog/7872.doc.rst new file mode 100644 index 00000000000..46236acbf2a --- /dev/null +++ b/changelog/7872.doc.rst @@ -0,0 +1 @@ +``_pytest.config.argparsing.Parser.addini()`` accepts explicit ``None`` and ``"string"``. From 43b1eb3c9eef41c9e7fdc7c56aac668ad661c9a3 Mon Sep 17 00:00:00 2001 From: Tanvi Mehta Date: Wed, 7 Oct 2020 21:51:28 -0700 Subject: [PATCH 684/823] Use instead of a in Issue #7868 Use `collections.Counter` instead of a `dict` in `terminal.py` Issue #7868 --- src/_pytest/terminal.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 0e8db20c760..f96169c967c 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -8,6 +8,7 @@ import platform import sys import warnings +from collections import Counter from functools import partial from pathlib import Path from typing import Any @@ -754,10 +755,7 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: # because later versions are going to get rid of them anyway. if self.config.option.verbose < 0: if self.config.option.verbose < -1: - counts: Dict[str, int] = {} - for item in items: - name = item.nodeid.split("::", 1)[0] - counts[name] = counts.get(name, 0) + 1 + counts = Counter(item.nodeid.split("::", 1)[0] for item in items) for name, count in sorted(counts.items()): self._tw.line("%s: %d" % (name, count)) else: @@ -922,10 +920,7 @@ def collapsed_location_report(reports: List[WarningReport]) -> str: if len(locations) < 10: return "\n".join(map(str, locations)) - counts_by_filename: Dict[str, int] = {} - for loc in locations: - key = str(loc).split("::", 1)[0] - counts_by_filename[key] = counts_by_filename.get(key, 0) + 1 + counts_by_filename = Counter(str(loc).split("::", 1)[0] for loc in locations) return "\n".join( "{}: {} warning{}".format(k, v, "s" if v > 1 else "") for k, v in counts_by_filename.items() From 779b511bfe10f5e212e761460f36edc8d4e65062 Mon Sep 17 00:00:00 2001 From: Tanvi Mehta Date: Wed, 7 Oct 2020 22:25:27 -0700 Subject: [PATCH 685/823] Fixed formatting --- src/_pytest/terminal.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index f96169c967c..ff28be56578 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -920,7 +920,9 @@ def collapsed_location_report(reports: List[WarningReport]) -> str: if len(locations) < 10: return "\n".join(map(str, locations)) - counts_by_filename = Counter(str(loc).split("::", 1)[0] for loc in locations) + counts_by_filename = Counter( + str(loc).split("::", 1)[0] for loc in locations + ) return "\n".join( "{}: {} warning{}".format(k, v, "s" if v > 1 else "") for k, v in counts_by_filename.items() From d0939314648e4168b7e621afebb153485bde28de Mon Sep 17 00:00:00 2001 From: Tanvi Mehta Date: Wed, 7 Oct 2020 22:57:52 -0700 Subject: [PATCH 686/823] Added name to authors list --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index ab84a3e5289..37c1e3c062b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -275,6 +275,7 @@ Sven-Hendrik Haase Sylvain Marié Tadek Teleżyński Takafumi Arakaki +Tanvi Mehta Tarcisio Fischer Tareq Alayan Ted Xiao From 2e322f183c95bcd32b64fba2911c0c81b23d1609 Mon Sep 17 00:00:00 2001 From: Charles Aracil Date: Fri, 9 Oct 2020 16:09:52 +0200 Subject: [PATCH 687/823] ask for commit after changelog and authors file edit (#7878) --- CONTRIBUTING.rst | 11 +++++------ changelog/7878.doc.rst | 1 + 2 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 changelog/7878.doc.rst diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 48ba147b7db..2669cb19509 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -299,12 +299,6 @@ Here is a simple overview, with pytest-specific bits: $ pytest testing/test_config.py - -#. Commit and push once your tests pass and you are happy with your change(s):: - - $ git commit -a -m "" - $ git push -u - #. Create a new changelog entry in ``changelog``. The file should be named ``..rst``, where *issueid* is the number of the issue related to the change and *type* is one of ``feature``, ``improvement``, ``bugfix``, ``doc``, ``deprecation``, ``breaking``, ``vendor`` @@ -313,6 +307,11 @@ Here is a simple overview, with pytest-specific bits: #. Add yourself to ``AUTHORS`` file if not there yet, in alphabetical order. +#. Commit and push once your tests pass and you are happy with your change(s):: + + $ git commit -a -m "" + $ git push -u + #. Finally, submit a pull request through the GitHub website using this data:: head-fork: YOUR_GITHUB_USERNAME/pytest diff --git a/changelog/7878.doc.rst b/changelog/7878.doc.rst new file mode 100644 index 00000000000..ff5d00d6c02 --- /dev/null +++ b/changelog/7878.doc.rst @@ -0,0 +1 @@ +In pull request section, ask to commit after editing changelog and authors file. From 3059caf1eeb34fa0c2e645832e52c29d156488ca Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 10 Oct 2020 18:51:35 +0300 Subject: [PATCH 688/823] Put smoke test deps in requirements.txt for Dependabot (#7806) --- .github/dependabot.yml | 11 +++++++++++ testing/plugins_integration/requirements.txt | 15 +++++++++++++++ tox.ini | 17 +---------------- 3 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 testing/plugins_integration/requirements.txt diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..507789bf5a4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/testing/plugins_integration" + schedule: + interval: weekly + time: "03:00" + open-pull-requests-limit: 10 + allow: + - dependency-type: direct + - dependency-type: indirect diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt new file mode 100644 index 00000000000..0763d24e719 --- /dev/null +++ b/testing/plugins_integration/requirements.txt @@ -0,0 +1,15 @@ +anyio[curio,trio]==2.0.0 +django==3.1.1 +pytest-asyncio==0.14.0 +pytest-bdd==4.0.1 +pytest-cov==2.10.1 +pytest-django==3.10.0 +pytest-flakes==4.0.2 +pytest-html==2.1.1 +pytest-mock==3.3.1 +pytest-rerunfailures==9.1.1 +pytest-sugar==0.9.4 +pytest-trio==0.6.0 +pytest-twisted==1.13.2 +twisted==20.3.0 +pytest-xvfb==2.0.0 diff --git a/tox.ini b/tox.ini index daaf9a7b6e3..5a4a85e558d 100644 --- a/tox.ini +++ b/tox.ini @@ -119,22 +119,7 @@ pip_pre=true download=true install_command=python -m pip --use-feature=2020-resolver install {opts} {packages} changedir = testing/plugins_integration -deps = - anyio[curio,trio] - django - pytest-asyncio - pytest-bdd - pytest-cov - pytest-django - pytest-flakes - pytest-html - pytest-mock - pytest-rerunfailures - pytest-sugar - pytest-trio - pytest-twisted - twisted - pytest-xvfb +deps = -rtesting/plugins_integration/requirements.txt setenv = PYTHONPATH=. # due to pytest-rerunfailures requiring 6.2+; can be removed after 6.2.0 From 37cf4693cf58b59407604c6cc0fab82f6ee6750d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2020 15:52:11 +0000 Subject: [PATCH 689/823] build(deps): bump anyio[curio,trio] in /testing/plugins_integration Bumps [anyio[curio,trio]](https://github.com/agronholm/anyio) from 2.0.0 to 2.0.2. - [Release notes](https://github.com/agronholm/anyio/releases) - [Changelog](https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst) - [Commits](https://github.com/agronholm/anyio/compare/2.0.0...2.0.2) 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 0763d24e719..d7f0e71e794 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,4 +1,4 @@ -anyio[curio,trio]==2.0.0 +anyio[curio,trio]==2.0.2 django==3.1.1 pytest-asyncio==0.14.0 pytest-bdd==4.0.1 From 008863aeb91a5c5fb0c5af822387c6137c75b434 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 10 Oct 2020 18:55:45 +0300 Subject: [PATCH 690/823] release-on-comment: add "Closes " to release PR --- scripts/release-on-comment.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/release-on-comment.py b/scripts/release-on-comment.py index e2ddbe1ca04..44431a4fc3f 100644 --- a/scripts/release-on-comment.py +++ b/scripts/release-on-comment.py @@ -57,6 +57,8 @@ class InvalidFeatureRelease(Exception): Once all builds pass and it has been **approved** by one or more maintainers, the build can be released by pushing a tag `{version}` to this repository. + +Closes #{issue_number}. """ @@ -164,7 +166,9 @@ def trigger_release(payload_path: Path, token: str) -> None: print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} pushed.") body = PR_BODY.format( - comment_url=get_comment_data(payload)["html_url"], version=version + comment_url=get_comment_data(payload)["html_url"], + version=version, + issue_number=issue_number, ) pr = repo.create_pull( f"Prepare release {version}", From b53a8bb60f34d38f0e1c9aa86f4dd61f918c6777 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2020 12:39:31 +0000 Subject: [PATCH 691/823] build(deps): bump django in /testing/plugins_integration Bumps [django](https://github.com/django/django) from 3.1.1 to 3.1.2. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.1.1...3.1.2) 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 d7f0e71e794..fcc8767006c 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,5 +1,5 @@ anyio[curio,trio]==2.0.2 -django==3.1.1 +django==3.1.2 pytest-asyncio==0.14.0 pytest-bdd==4.0.1 pytest-cov==2.10.1 From 69419cb70007514e5061f3b8d359b7da9dff82dd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 12 Oct 2020 12:13:06 -0300 Subject: [PATCH 692/823] New pytester fixture (#7854) --- changelog/7425.feature.rst | 5 + doc/en/reference.rst | 27 +- src/_pytest/pytester.py | 491 +++++++++++++++++++++++++---------- src/_pytest/warning_types.py | 3 - testing/acceptance_test.py | 6 +- testing/test_doctest.py | 476 ++++++++++++++++----------------- testing/test_pytester.py | 13 +- 7 files changed, 632 insertions(+), 389 deletions(-) create mode 100644 changelog/7425.feature.rst diff --git a/changelog/7425.feature.rst b/changelog/7425.feature.rst new file mode 100644 index 00000000000..55881d2074f --- /dev/null +++ b/changelog/7425.feature.rst @@ -0,0 +1,5 @@ +New :fixture:`pytester` fixture, which is identical to :fixture:`testdir` but its methods return :class:`pathlib.Path` when appropriate instead of ``py.path.local``. + +This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future. + +Internally, the old :class:`Testdir` is now a thin wrapper around :class:`Pytester`, preserving the old interface. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 15d8250844f..6795b721c8f 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -499,17 +499,21 @@ monkeypatch :members: -.. fixture:: testdir +.. fixture:: pytester -testdir -~~~~~~~ +pytester +~~~~~~~~ + +.. versionadded:: 6.2 .. currentmodule:: _pytest.pytester -This fixture provides a :class:`Testdir` instance useful for black-box testing of test files, making it ideal to -test plugins. +Provides a :class:`Pytester` instance that can be used to run and test pytest itself. + +It provides an empty directory where pytest can be executed in isolation, and contains facilities +to write tests, configuration files, and match against expected output. -To use it, include in your top-most ``conftest.py`` file: +To use it, include in your topmost ``conftest.py`` file: .. code-block:: python @@ -517,7 +521,7 @@ To use it, include in your top-most ``conftest.py`` file: -.. autoclass:: Testdir() +.. autoclass:: Pytester() :members: .. autoclass:: RunResult() @@ -526,6 +530,15 @@ To use it, include in your top-most ``conftest.py`` file: .. autoclass:: LineMatcher() :members: +.. fixture:: testdir + +testdir +~~~~~~~ + +Identical to :fixture:`pytester`, but provides an instance whose methods return +legacy ``py.path.local`` objects instead when applicable. + +New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. .. fixture:: recwarn diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index e66e718f100..b7a79b90299 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1,16 +1,19 @@ """(Disabled by default) support for testing pytest and pytest plugins.""" import collections.abc +import contextlib import gc import importlib import os import platform import re +import shutil import subprocess import sys import traceback from fnmatch import fnmatch from io import StringIO from pathlib import Path +from typing import Any from typing import Callable from typing import Dict from typing import Generator @@ -19,12 +22,14 @@ from typing import Optional from typing import overload from typing import Sequence +from typing import TextIO from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union from weakref import WeakKeyDictionary +import attr import py from iniconfig import IniConfig @@ -47,7 +52,7 @@ from _pytest.python import Module from _pytest.reports import CollectReport from _pytest.reports import TestReport -from _pytest.tmpdir import TempdirFactory +from _pytest.tmpdir import TempPathFactory if TYPE_CHECKING: from typing_extensions import Literal @@ -176,11 +181,11 @@ def _pytest(request: FixtureRequest) -> "PytestArg": class PytestArg: def __init__(self, request: FixtureRequest) -> None: - self.request = request + self._request = request def gethookrecorder(self, hook) -> "HookRecorder": hookrecorder = HookRecorder(hook._pm) - self.request.addfinalizer(hookrecorder.finish_recording) + self._request.addfinalizer(hookrecorder.finish_recording) return hookrecorder @@ -430,13 +435,29 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: @pytest.fixture -def testdir(request: FixtureRequest, tmpdir_factory: TempdirFactory) -> "Testdir": - """A :class: `TestDir` instance, that can be used to run and test pytest itself. +def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester": + """ + Facilities to write tests/configuration files, execute pytest in isolation, and match + against expected output, perfect for black-box testing of pytest plugins. + + It attempts to isolate the test run from external factors as much as possible, modifying + the current working directory to ``path`` and environment variables during initialization. + + It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path` + fixture but provides methods which aid in testing pytest itself. + """ + return Pytester(request, tmp_path_factory) + - It is particularly useful for testing plugins. It is similar to the `tmpdir` fixture - but provides methods which aid in testing pytest itself. +@pytest.fixture +def testdir(pytester: "Pytester") -> "Testdir": + """ + Identical to :fixture:`pytester`, and provides an instance whose methods return + legacy ``py.path.local`` objects instead when applicable. + + New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. """ - return Testdir(request, tmpdir_factory) + return Testdir(pytester) @pytest.fixture @@ -599,16 +620,17 @@ def restore(self) -> None: @final -class Testdir: - """Temporary test directory with tools to test/run pytest itself. +class Pytester: + """ + Facilities to write tests/configuration files, execute pytest in isolation, and match + against expected output, perfect for black-box testing of pytest plugins. - This is based on the :fixture:`tmpdir` fixture but provides a number of methods - which aid with testing pytest itself. Unless :py:meth:`chdir` is used all - methods will use :py:attr:`tmpdir` as their current working directory. + It attempts to isolate the test run from external factors as much as possible, modifying + the current working directory to ``path`` and environment variables during initialization. Attributes: - :ivar tmpdir: The :py:class:`py.path.local` instance of the temporary directory. + :ivar Path path: temporary directory path used to create files/run tests from, etc. :ivar plugins: A list of plugins to use with :py:meth:`parseconfig` and @@ -624,8 +646,10 @@ class Testdir: class TimeoutExpired(Exception): pass - def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> None: - self.request = request + def __init__( + self, request: FixtureRequest, tmp_path_factory: TempPathFactory + ) -> None: + self._request = request self._mod_collections: WeakKeyDictionary[ Module, List[Union[Item, Collector]] ] = (WeakKeyDictionary()) @@ -634,46 +658,49 @@ def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> N else: name = request.node.name self._name = name - self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) - self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) + self._path: Path = tmp_path_factory.mktemp(name, numbered=True) self.plugins: List[Union[str, _PluggyPlugin]] = [] self._cwd_snapshot = CwdSnapshot() self._sys_path_snapshot = SysPathsSnapshot() self._sys_modules_snapshot = self.__take_sys_modules_snapshot() self.chdir() - self.request.addfinalizer(self.finalize) - self._method = self.request.config.getoption("--runpytest") + self._request.addfinalizer(self._finalize) + self._method = self._request.config.getoption("--runpytest") + self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True) - mp = self.monkeypatch = MonkeyPatch() - mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self.test_tmproot)) + self._monkeypatch = mp = MonkeyPatch() + mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot)) # Ensure no unexpected caching via tox. mp.delenv("TOX_ENV_DIR", raising=False) # Discard outer pytest options. mp.delenv("PYTEST_ADDOPTS", raising=False) # Ensure no user config is used. - tmphome = str(self.tmpdir) + tmphome = str(self.path) mp.setenv("HOME", tmphome) mp.setenv("USERPROFILE", tmphome) # Do not use colors for inner runs by default. mp.setenv("PY_COLORS", "0") - def __repr__(self) -> str: - return f"" + @property + def path(self) -> Path: + """Temporary directory where files are created and pytest is executed.""" + return self._path - def __str__(self) -> str: - return str(self.tmpdir) + def __repr__(self) -> str: + return f"" - def finalize(self) -> None: - """Clean up global state artifacts. + def _finalize(self) -> None: + """ + Clean up global state artifacts. Some methods modify the global interpreter state and this tries to - clean this up. It does not remove the temporary directory however so + clean this up. It does not remove the temporary directory however so it can be looked at after the test run has finished. """ self._sys_modules_snapshot.restore() self._sys_path_snapshot.restore() self._cwd_snapshot.restore() - self.monkeypatch.undo() + self._monkeypatch.undo() def __take_sys_modules_snapshot(self) -> SysModulesSnapshot: # Some zope modules used by twisted-related tests keep internal state @@ -687,7 +714,7 @@ def preserve_module(name): def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder: """Create a new :py:class:`HookRecorder` for a PluginManager.""" pluginmanager.reprec = reprec = HookRecorder(pluginmanager) - self.request.addfinalizer(reprec.finish_recording) + self._request.addfinalizer(reprec.finish_recording) return reprec def chdir(self) -> None: @@ -695,12 +722,18 @@ def chdir(self) -> None: This is done automatically upon instantiation. """ - self.tmpdir.chdir() + os.chdir(self.path) - def _makefile(self, ext: str, lines, files, encoding: str = "utf-8"): + def _makefile( + self, + ext: str, + lines: Sequence[Union[Any, bytes]], + files: Dict[str, str], + encoding: str = "utf-8", + ) -> Path: items = list(files.items()) - def to_text(s): + def to_text(s: Union[Any, bytes]) -> str: return s.decode(encoding) if isinstance(s, bytes) else str(s) if lines: @@ -710,17 +743,18 @@ def to_text(s): ret = None for basename, value in items: - p = self.tmpdir.join(basename).new(ext=ext) - p.dirpath().ensure_dir() + p = self.path.joinpath(basename).with_suffix(ext) + p.parent.mkdir(parents=True, exist_ok=True) source_ = Source(value) source = "\n".join(to_text(line) for line in source_.lines) - p.write(source.strip().encode(encoding), "wb") + p.write_text(source.strip(), encoding=encoding) if ret is None: ret = p + assert ret is not None return ret - def makefile(self, ext: str, *args: str, **kwargs): - r"""Create new file(s) in the testdir. + def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: + r"""Create new file(s) in the test directory. :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`. @@ -743,27 +777,27 @@ def makefile(self, ext: str, *args: str, **kwargs): """ return self._makefile(ext, args, kwargs) - def makeconftest(self, source): + def makeconftest(self, source: str) -> Path: """Write a contest.py file with 'source' as contents.""" return self.makepyfile(conftest=source) - def makeini(self, source): + def makeini(self, source: str) -> Path: """Write a tox.ini file with 'source' as contents.""" return self.makefile(".ini", tox=source) - def getinicfg(self, source) -> IniConfig: + def getinicfg(self, source: str) -> IniConfig: """Return the pytest section from the tox.ini config file.""" p = self.makeini(source) return IniConfig(p)["pytest"] - def makepyprojecttoml(self, source): + def makepyprojecttoml(self, source: str) -> Path: """Write a pyproject.toml file with 'source' as contents. .. versionadded:: 6.0 """ return self.makefile(".toml", pyproject=source) - def makepyfile(self, *args, **kwargs): + def makepyfile(self, *args, **kwargs) -> Path: r"""Shortcut for .makefile() with a .py extension. Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting @@ -783,7 +817,7 @@ def test_something(testdir): """ return self._makefile(".py", args, kwargs) - def maketxtfile(self, *args, **kwargs): + def maketxtfile(self, *args, **kwargs) -> Path: r"""Shortcut for .makefile() with a .txt extension. Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting @@ -803,74 +837,77 @@ def test_something(testdir): """ return self._makefile(".txt", args, kwargs) - def syspathinsert(self, path=None) -> None: + def syspathinsert( + self, path: Optional[Union[str, "os.PathLike[str]"]] = None + ) -> None: """Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`. This is undone automatically when this object dies at the end of each test. """ if path is None: - path = self.tmpdir + path = self.path - self.monkeypatch.syspath_prepend(str(path)) + self._monkeypatch.syspath_prepend(str(path)) - def mkdir(self, name) -> py.path.local: + def mkdir(self, name: str) -> Path: """Create a new (sub)directory.""" - return self.tmpdir.mkdir(name) + p = self.path / name + p.mkdir() + return p - def mkpydir(self, name) -> py.path.local: - """Create a new Python package. + def mkpydir(self, name: str) -> Path: + """Create a new python package. This creates a (sub)directory with an empty ``__init__.py`` file so it gets recognised as a Python package. """ - p = self.mkdir(name) - p.ensure("__init__.py") + p = self.path / name + p.mkdir() + p.joinpath("__init__.py").touch() return p - def copy_example(self, name=None) -> py.path.local: + def copy_example(self, name: Optional[str] = None) -> Path: """Copy file from project's directory into the testdir. :param str name: The name of the file to copy. - :returns: Path to the copied directory (inside ``self.tmpdir``). - """ - import warnings - from _pytest.warning_types import PYTESTER_COPY_EXAMPLE + :return: path to the copied directory (inside ``self.path``). - warnings.warn(PYTESTER_COPY_EXAMPLE, stacklevel=2) - example_dir = self.request.config.getini("pytester_example_dir") + """ + example_dir = self._request.config.getini("pytester_example_dir") if example_dir is None: raise ValueError("pytester_example_dir is unset, can't copy examples") - example_dir = self.request.config.rootdir.join(example_dir) + example_dir = Path(str(self._request.config.rootdir)) / example_dir - for extra_element in self.request.node.iter_markers("pytester_example_path"): + for extra_element in self._request.node.iter_markers("pytester_example_path"): assert extra_element.args - example_dir = example_dir.join(*extra_element.args) + example_dir = example_dir.joinpath(*extra_element.args) if name is None: func_name = self._name maybe_dir = example_dir / func_name maybe_file = example_dir / (func_name + ".py") - if maybe_dir.isdir(): + if maybe_dir.is_dir(): example_path = maybe_dir - elif maybe_file.isfile(): + elif maybe_file.is_file(): example_path = maybe_file else: raise LookupError( - "{} cant be found as module or package in {}".format( - func_name, example_dir.bestrelpath(self.request.config.rootdir) - ) + f"{func_name} can't be found as module or package in {example_dir}" ) else: - example_path = example_dir.join(name) - - if example_path.isdir() and not example_path.join("__init__.py").isfile(): - example_path.copy(self.tmpdir) - return self.tmpdir - elif example_path.isfile(): - result = self.tmpdir.join(example_path.basename) - example_path.copy(result) + example_path = example_dir.joinpath(name) + + if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): + # TODO: py.path.local.copy can copy files to existing directories, + # while with shutil.copytree the destination directory cannot exist, + # we will need to roll our own in order to drop py.path.local completely + py.path.local(example_path).copy(py.path.local(self.path)) + return self.path + elif example_path.is_file(): + result = self.path.joinpath(example_path.name) + shutil.copy(example_path, result) return result else: raise LookupError( @@ -879,7 +916,9 @@ def copy_example(self, name=None) -> py.path.local: Session = Session - def getnode(self, config: Config, arg): + def getnode( + self, config: Config, arg: Union[str, "os.PathLike[str]"] + ) -> Optional[Union[Collector, Item]]: """Return the collection node of a file. :param _pytest.config.Config config: @@ -896,7 +935,7 @@ def getnode(self, config: Config, arg): config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) return res - def getpathnode(self, path): + def getpathnode(self, path: Union[str, "os.PathLike[str]"]): """Return the collection node of a file. This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to @@ -904,6 +943,7 @@ def getpathnode(self, path): :param py.path.local path: Path to the file. """ + path = py.path.local(path) config = self.parseconfigure(path) session = Session.from_config(config) x = session.fspath.bestrelpath(path) @@ -924,7 +964,7 @@ def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]: result.extend(session.genitems(colitem)) return result - def runitem(self, source): + def runitem(self, source: str) -> Any: """Run the "test_func" Item. The calling test instance (class containing the test method) must @@ -935,11 +975,11 @@ def runitem(self, source): # used from runner functional tests item = self.getitem(source) # the test class where we are called from wants to provide the runner - testclassinstance = self.request.instance + testclassinstance = self._request.instance runner = testclassinstance.getrunner() return runner(item) - def inline_runsource(self, source, *cmdlineargs) -> HookRecorder: + def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder: """Run a test module in process using ``pytest.main()``. This run writes "source" into a temporary file and runs @@ -968,7 +1008,10 @@ def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]: return items, rec def inline_run( - self, *args, plugins=(), no_reraise_ctrlc: bool = False + self, + *args: Union[str, "os.PathLike[str]"], + plugins=(), + no_reraise_ctrlc: bool = False, ) -> HookRecorder: """Run ``pytest.main()`` in-process, returning a HookRecorder. @@ -1016,7 +1059,7 @@ def pytest_configure(x, config: Config) -> None: rec.append(self.make_hook_recorder(config.pluginmanager)) plugins.append(Collect()) - ret = pytest.main(list(args), plugins=plugins) + ret = pytest.main([str(x) for x in args], plugins=plugins) if len(rec) == 1: reprec = rec.pop() else: @@ -1024,7 +1067,7 @@ def pytest_configure(x, config: Config) -> None: class reprec: # type: ignore pass - reprec.ret = ret + reprec.ret = ret # type: ignore # Typically we reraise keyboard interrupts from the child run # because it's our user requesting interruption of the testing. @@ -1037,7 +1080,9 @@ class reprec: # type: ignore for finalizer in finalizers: finalizer() - def runpytest_inprocess(self, *args, **kwargs) -> RunResult: + def runpytest_inprocess( + self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any + ) -> RunResult: """Return result of running pytest in-process, providing a similar interface to what self.runpytest() provides.""" syspathinsert = kwargs.pop("syspathinsert", False) @@ -1079,26 +1124,30 @@ class reprec: # type: ignore res.reprec = reprec # type: ignore return res - def runpytest(self, *args, **kwargs) -> RunResult: + def runpytest( + self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any + ) -> RunResult: """Run pytest inline or in a subprocess, depending on the command line option "--runpytest" and return a :py:class:`RunResult`.""" - args = self._ensure_basetemp(args) + new_args = self._ensure_basetemp(args) if self._method == "inprocess": - return self.runpytest_inprocess(*args, **kwargs) + return self.runpytest_inprocess(*new_args, **kwargs) elif self._method == "subprocess": - return self.runpytest_subprocess(*args, **kwargs) + return self.runpytest_subprocess(*new_args, **kwargs) raise RuntimeError(f"Unrecognized runpytest option: {self._method}") - def _ensure_basetemp(self, args): - args = list(args) - for x in args: + def _ensure_basetemp( + self, args: Sequence[Union[str, "os.PathLike[str]"]] + ) -> List[Union[str, "os.PathLike[str]"]]: + new_args = list(args) + for x in new_args: if str(x).startswith("--basetemp"): break else: - args.append("--basetemp=%s" % self.tmpdir.dirpath("basetemp")) - return args + new_args.append("--basetemp=%s" % self.path.parent.joinpath("basetemp")) + return new_args - def parseconfig(self, *args) -> Config: + def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config: """Return a new pytest Config instance from given commandline args. This invokes the pytest bootstrapping code in _pytest.config to create @@ -1109,18 +1158,19 @@ def parseconfig(self, *args) -> Config: If :py:attr:`plugins` has been populated they should be plugin modules to be registered with the PluginManager. """ - args = self._ensure_basetemp(args) - import _pytest.config - config = _pytest.config._prepareconfig(args, self.plugins) # type: ignore[arg-type] + new_args = self._ensure_basetemp(args) + new_args = [str(x) for x in new_args] + + config = _pytest.config._prepareconfig(new_args, self.plugins) # type: ignore[arg-type] # we don't know what the test will do with this half-setup config # object and thus we make sure it gets unconfigured properly in any # case (otherwise capturing could still be active, for example) - self.request.addfinalizer(config._ensure_unconfigure) + self._request.addfinalizer(config._ensure_unconfigure) return config - def parseconfigure(self, *args) -> Config: + def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config: """Return a new pytest configured Config instance. Returns a new :py:class:`_pytest.config.Config` instance like @@ -1130,7 +1180,7 @@ def parseconfigure(self, *args) -> Config: config._do_configure() return config - def getitem(self, source, funcname: str = "test_func") -> Item: + def getitem(self, source: str, funcname: str = "test_func") -> Item: """Return the test item for a test function. Writes the source to a python file and runs pytest's collection on @@ -1150,7 +1200,7 @@ def getitem(self, source, funcname: str = "test_func") -> Item: funcname, source, items ) - def getitems(self, source) -> List[Item]: + def getitems(self, source: str) -> List[Item]: """Return all test items collected from the module. Writes the source to a Python file and runs pytest's collection on @@ -1159,7 +1209,9 @@ def getitems(self, source) -> List[Item]: modcol = self.getmodulecol(source) return self.genitems([modcol]) - def getmodulecol(self, source, configargs=(), withinit: bool = False): + def getmodulecol( + self, source: Union[str, Path], configargs=(), *, withinit: bool = False + ): """Return the module collection node for ``source``. Writes ``source`` to a file using :py:meth:`makepyfile` and then @@ -1177,10 +1229,10 @@ def getmodulecol(self, source, configargs=(), withinit: bool = False): directory to ensure it is a package. """ if isinstance(source, Path): - path = self.tmpdir.join(str(source)) + path = self.path.joinpath(source) assert not withinit, "not supported for paths" else: - kw = {self._name: Source(source).strip()} + kw = {self._name: str(source)} path = self.makepyfile(**kw) if withinit: self.makepyfile(__init__="#") @@ -1208,8 +1260,8 @@ def collect_by_name( def popen( self, cmdargs, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + stdout: Union[int, TextIO] = subprocess.PIPE, + stderr: Union[int, TextIO] = subprocess.PIPE, stdin=CLOSE_STDIN, **kw, ): @@ -1244,14 +1296,18 @@ def popen( return popen def run( - self, *cmdargs, timeout: Optional[float] = None, stdin=CLOSE_STDIN + self, + *cmdargs: Union[str, "os.PathLike[str]"], + timeout: Optional[float] = None, + stdin=CLOSE_STDIN, ) -> RunResult: """Run a command with arguments. Run a process using subprocess.Popen saving the stdout and stderr. - :param args: - The sequence of arguments to pass to `subprocess.Popen()`. + :param cmdargs: + The sequence of arguments to pass to `subprocess.Popen()`, with path-like objects + being converted to ``str`` automatically. :param timeout: The period in seconds after which to timeout and raise :py:class:`Testdir.TimeoutExpired`. @@ -1266,15 +1322,14 @@ def run( __tracebackhide__ = True cmdargs = tuple( - str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs + os.fspath(arg) if isinstance(arg, os.PathLike) else arg for arg in cmdargs ) - p1 = self.tmpdir.join("stdout") - p2 = self.tmpdir.join("stderr") + p1 = self.path.joinpath("stdout") + p2 = self.path.joinpath("stderr") print("running:", *cmdargs) - print(" in:", py.path.local()) - f1 = open(str(p1), "w", encoding="utf8") - f2 = open(str(p2), "w", encoding="utf8") - try: + print(" in:", Path.cwd()) + + with p1.open("w", encoding="utf8") as f1, p2.open("w", encoding="utf8") as f2: now = timing.time() popen = self.popen( cmdargs, @@ -1305,23 +1360,16 @@ def handle_timeout() -> None: ret = popen.wait(timeout) except subprocess.TimeoutExpired: handle_timeout() - finally: - f1.close() - f2.close() - f1 = open(str(p1), encoding="utf8") - f2 = open(str(p2), encoding="utf8") - try: + + with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2: out = f1.read().splitlines() err = f2.read().splitlines() - finally: - f1.close() - f2.close() + self._dump_lines(out, sys.stdout) self._dump_lines(err, sys.stderr) - try: + + with contextlib.suppress(ValueError): ret = ExitCode(ret) - except ValueError: - pass return RunResult(ret, out, err, timing.time() - now) def _dump_lines(self, lines, fp): @@ -1366,7 +1414,7 @@ def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunRes :rtype: RunResult """ __tracebackhide__ = True - p = make_numbered_dir(root=Path(str(self.tmpdir)), prefix="runpytest-") + p = make_numbered_dir(root=self.path, prefix="runpytest-") args = ("--basetemp=%s" % p,) + args plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: @@ -1384,7 +1432,8 @@ def spawn_pytest( The pexpect child is returned. """ - basetemp = self.tmpdir.mkdir("temp-pexpect") + basetemp = self.path / "temp-pexpect" + basetemp.mkdir() invoke = " ".join(map(str, self._getpytestargs())) cmd = f"{invoke} --basetemp={basetemp} {string}" return self.spawn(cmd, expect_timeout=expect_timeout) @@ -1399,10 +1448,10 @@ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": pytest.skip("pypy-64 bit not supported") if not hasattr(pexpect, "spawn"): pytest.skip("pexpect.spawn not available") - logfile = self.tmpdir.join("spawn.out").open("wb") + logfile = self.path.joinpath("spawn.out").open("wb") child = pexpect.spawn(cmd, logfile=logfile) - self.request.addfinalizer(logfile.close) + self._request.addfinalizer(logfile.close) child.timeout = expect_timeout return child @@ -1425,6 +1474,178 @@ def assert_contains_lines(self, lines2: Sequence[str]) -> None: LineMatcher(lines1).fnmatch_lines(lines2) +@final +@attr.s(repr=False, str=False) +class Testdir: + """ + Similar to :class:`Pytester`, but this class works with legacy py.path.local objects instead. + + All methods just forward to an internal :class:`Pytester` instance, converting results + to `py.path.local` objects as necessary. + """ + + __test__ = False + + CLOSE_STDIN = Pytester.CLOSE_STDIN + TimeoutExpired = Pytester.TimeoutExpired + Session = Pytester.Session + + _pytester: Pytester = attr.ib() + + @property + def tmpdir(self) -> py.path.local: + return py.path.local(self._pytester.path) + + @property + def test_tmproot(self) -> py.path.local: + return py.path.local(self._pytester._test_tmproot) + + @property + def request(self): + return self._pytester._request + + @property + def plugins(self): + return self._pytester.plugins + + @plugins.setter + def plugins(self, plugins): + self._pytester.plugins = plugins + + @property + def monkeypatch(self) -> MonkeyPatch: + return self._pytester._monkeypatch + + def make_hook_recorder(self, pluginmanager) -> HookRecorder: + return self._pytester.make_hook_recorder(pluginmanager) + + def chdir(self) -> None: + return self._pytester.chdir() + + def finalize(self) -> None: + return self._pytester._finalize() + + def makefile(self, ext, *args, **kwargs) -> py.path.local: + return py.path.local(str(self._pytester.makefile(ext, *args, **kwargs))) + + def makeconftest(self, source) -> py.path.local: + return py.path.local(str(self._pytester.makeconftest(source))) + + def makeini(self, source) -> py.path.local: + return py.path.local(str(self._pytester.makeini(source))) + + def getinicfg(self, source) -> py.path.local: + return py.path.local(str(self._pytester.getinicfg(source))) + + def makepyprojecttoml(self, source) -> py.path.local: + return py.path.local(str(self._pytester.makepyprojecttoml(source))) + + def makepyfile(self, *args, **kwargs) -> py.path.local: + return py.path.local(str(self._pytester.makepyfile(*args, **kwargs))) + + def maketxtfile(self, *args, **kwargs) -> py.path.local: + return py.path.local(str(self._pytester.maketxtfile(*args, **kwargs))) + + def syspathinsert(self, path=None) -> None: + return self._pytester.syspathinsert(path) + + def mkdir(self, name) -> py.path.local: + return py.path.local(str(self._pytester.mkdir(name))) + + def mkpydir(self, name) -> py.path.local: + return py.path.local(str(self._pytester.mkpydir(name))) + + def copy_example(self, name=None) -> py.path.local: + return py.path.local(str(self._pytester.copy_example(name))) + + def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]: + return self._pytester.getnode(config, arg) + + def getpathnode(self, path): + return self._pytester.getpathnode(path) + + def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]: + return self._pytester.genitems(colitems) + + def runitem(self, source): + return self._pytester.runitem(source) + + def inline_runsource(self, source, *cmdlineargs): + return self._pytester.inline_runsource(source, *cmdlineargs) + + def inline_genitems(self, *args): + return self._pytester.inline_genitems(*args) + + def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): + return self._pytester.inline_run( + *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc + ) + + def runpytest_inprocess(self, *args, **kwargs) -> RunResult: + return self._pytester.runpytest_inprocess(*args, **kwargs) + + def runpytest(self, *args, **kwargs) -> RunResult: + return self._pytester.runpytest(*args, **kwargs) + + def parseconfig(self, *args) -> Config: + return self._pytester.parseconfig(*args) + + def parseconfigure(self, *args) -> Config: + return self._pytester.parseconfigure(*args) + + def getitem(self, source, funcname="test_func"): + return self._pytester.getitem(source, funcname) + + def getitems(self, source): + return self._pytester.getitems(source) + + def getmodulecol(self, source, configargs=(), withinit=False): + return self._pytester.getmodulecol( + source, configargs=configargs, withinit=withinit + ) + + def collect_by_name( + self, modcol: Module, name: str + ) -> Optional[Union[Item, Collector]]: + return self._pytester.collect_by_name(modcol, name) + + def popen( + self, + cmdargs, + stdout: Union[int, TextIO] = subprocess.PIPE, + stderr: Union[int, TextIO] = subprocess.PIPE, + stdin=CLOSE_STDIN, + **kw, + ): + return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw) + + def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: + return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin) + + def runpython(self, script) -> RunResult: + return self._pytester.runpython(script) + + def runpython_c(self, command): + return self._pytester.runpython_c(command) + + def runpytest_subprocess(self, *args, timeout=None) -> RunResult: + return self._pytester.runpytest_subprocess(*args, timeout=timeout) + + def spawn_pytest( + self, string: str, expect_timeout: float = 10.0 + ) -> "pexpect.spawn": + return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout) + + def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": + return self._pytester.spawn(cmd, expect_timeout=expect_timeout) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return str(self.tmpdir) + + class LineMatcher: """Flexible matching of text. diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index bd3a1d0b720..2fd4d4f6e8f 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -108,6 +108,3 @@ class UnformattedWarning(Generic[_W]): def format(self, **kwargs: Any) -> _W: """Return an instance of the warning category, formatted with given kwargs.""" return self.category(self.template.format(**kwargs)) - - -PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example") diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index f4b7d6135ed..c937ce9dc1e 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -9,7 +9,7 @@ from _pytest.compat import importlib_metadata from _pytest.config import ExitCode from _pytest.pathlib import symlink_or_skip -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester def prepend_pythonpath(*dirs): @@ -1276,14 +1276,14 @@ def test_simple(): sys.platform == "win32", reason="Windows raises `OSError: [Errno 22] Invalid argument` instead", ) -def test_no_brokenpipeerror_message(testdir: Testdir) -> None: +def test_no_brokenpipeerror_message(pytester: Pytester) -> None: """Ensure that the broken pipe error message is supressed. In some Python versions, it reaches sys.unraisablehook, in others a BrokenPipeError exception is propagated, but either way it prints to stderr on shutdown, so checking nothing is printed is enough. """ - popen = testdir.popen((*testdir._getpytestargs(), "--help")) + popen = pytester.popen((*pytester._getpytestargs(), "--help")) popen.stdout.close() ret = popen.wait() assert popen.stderr.read() == b"" diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 0e8bba980f0..67e93b76a49 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -3,6 +3,8 @@ from typing import Callable from typing import Optional +import py + import pytest from _pytest.doctest import _get_checker from _pytest.doctest import _is_mocked @@ -11,12 +13,13 @@ from _pytest.doctest import DoctestItem from _pytest.doctest import DoctestModule from _pytest.doctest import DoctestTextfile +from _pytest.pytester import Pytester class TestDoctests: - def test_collect_testtextfile(self, testdir): - w = testdir.maketxtfile(whatever="") - checkfile = testdir.maketxtfile( + def test_collect_testtextfile(self, pytester: Pytester): + w = pytester.maketxtfile(whatever="") + checkfile = pytester.maketxtfile( test_something=""" alskdjalsdk >>> i = 5 @@ -25,48 +28,48 @@ def test_collect_testtextfile(self, testdir): """ ) - for x in (testdir.tmpdir, checkfile): + for x in (pytester.path, checkfile): # print "checking that %s returns custom items" % (x,) - items, reprec = testdir.inline_genitems(x) + items, reprec = pytester.inline_genitems(x) assert len(items) == 1 assert isinstance(items[0], DoctestItem) assert isinstance(items[0].parent, DoctestTextfile) # Empty file has no items. - items, reprec = testdir.inline_genitems(w) + items, reprec = pytester.inline_genitems(w) assert len(items) == 0 - def test_collect_module_empty(self, testdir): - path = testdir.makepyfile(whatever="#") - for p in (path, testdir.tmpdir): - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + def test_collect_module_empty(self, pytester: Pytester): + path = pytester.makepyfile(whatever="#") + for p in (path, pytester.path): + items, reprec = pytester.inline_genitems(p, "--doctest-modules") assert len(items) == 0 - def test_collect_module_single_modulelevel_doctest(self, testdir): - path = testdir.makepyfile(whatever='""">>> pass"""') - for p in (path, testdir.tmpdir): - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + def test_collect_module_single_modulelevel_doctest(self, pytester: Pytester): + path = pytester.makepyfile(whatever='""">>> pass"""') + for p in (path, pytester.path): + items, reprec = pytester.inline_genitems(p, "--doctest-modules") assert len(items) == 1 assert isinstance(items[0], DoctestItem) assert isinstance(items[0].parent, DoctestModule) - def test_collect_module_two_doctest_one_modulelevel(self, testdir): - path = testdir.makepyfile( + def test_collect_module_two_doctest_one_modulelevel(self, pytester: Pytester): + path = pytester.makepyfile( whatever=""" '>>> x = None' def my_func(): ">>> magic = 42 " """ ) - for p in (path, testdir.tmpdir): - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + for p in (path, pytester.path): + items, reprec = pytester.inline_genitems(p, "--doctest-modules") assert len(items) == 2 assert isinstance(items[0], DoctestItem) assert isinstance(items[1], DoctestItem) assert isinstance(items[0].parent, DoctestModule) assert items[0].parent is items[1].parent - def test_collect_module_two_doctest_no_modulelevel(self, testdir): - path = testdir.makepyfile( + def test_collect_module_two_doctest_no_modulelevel(self, pytester: Pytester): + path = pytester.makepyfile( whatever=""" '# Empty' def my_func(): @@ -83,72 +86,72 @@ def another(): ''' """ ) - for p in (path, testdir.tmpdir): - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + for p in (path, pytester.path): + items, reprec = pytester.inline_genitems(p, "--doctest-modules") assert len(items) == 2 assert isinstance(items[0], DoctestItem) assert isinstance(items[1], DoctestItem) assert isinstance(items[0].parent, DoctestModule) assert items[0].parent is items[1].parent - def test_simple_doctestfile(self, testdir): - p = testdir.maketxtfile( + def test_simple_doctestfile(self, pytester: Pytester): + p = pytester.maketxtfile( test_doc=""" >>> x = 1 >>> x == 1 False """ ) - reprec = testdir.inline_run(p) + reprec = pytester.inline_run(p) reprec.assertoutcome(failed=1) - def test_new_pattern(self, testdir): - p = testdir.maketxtfile( + def test_new_pattern(self, pytester: Pytester): + p = pytester.maketxtfile( xdoc=""" >>> x = 1 >>> x == 1 False """ ) - reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") + reprec = pytester.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(failed=1) - def test_multiple_patterns(self, testdir): + def test_multiple_patterns(self, pytester: Pytester): """Test support for multiple --doctest-glob arguments (#1255).""" - testdir.maketxtfile( + pytester.maketxtfile( xdoc=""" >>> 1 1 """ ) - testdir.makefile( + pytester.makefile( ".foo", test=""" >>> 1 1 """, ) - testdir.maketxtfile( + pytester.maketxtfile( test_normal=""" >>> 1 1 """ ) expected = {"xdoc.txt", "test.foo", "test_normal.txt"} - assert {x.basename for x in testdir.tmpdir.listdir()} == expected + assert {x.name for x in pytester.path.iterdir()} == expected args = ["--doctest-glob=xdoc*.txt", "--doctest-glob=*.foo"] - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) result.stdout.fnmatch_lines(["*test.foo *", "*xdoc.txt *", "*2 passed*"]) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*test_normal.txt *", "*1 passed*"]) @pytest.mark.parametrize( " test_string, encoding", [("foo", "ascii"), ("öäü", "latin1"), ("öäü", "utf-8")], ) - def test_encoding(self, testdir, test_string, encoding): + def test_encoding(self, pytester, test_string, encoding): """Test support for doctest_encoding ini option.""" - testdir.makeini( + pytester.makeini( """ [pytest] doctest_encoding={} @@ -162,21 +165,22 @@ def test_encoding(self, testdir, test_string, encoding): """.format( test_string, repr(test_string) ) - testdir._makefile(".txt", [doctest], {}, encoding=encoding) + fn = pytester.path / "test_encoding.txt" + fn.write_text(doctest, encoding=encoding) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - def test_doctest_unexpected_exception(self, testdir): - testdir.maketxtfile( + def test_doctest_unexpected_exception(self, pytester: Pytester): + pytester.maketxtfile( """ >>> i = 0 >>> 0 / i 2 """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "test_doctest_unexpected_exception.txt F *", @@ -196,8 +200,8 @@ def test_doctest_unexpected_exception(self, testdir): consecutive=True, ) - def test_doctest_outcomes(self, testdir): - testdir.maketxtfile( + def test_doctest_outcomes(self, pytester: Pytester): + pytester.maketxtfile( test_skip=""" >>> 1 1 @@ -219,7 +223,7 @@ def test_doctest_outcomes(self, testdir): bar """, ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "collected 3 items", @@ -232,11 +236,11 @@ def test_doctest_outcomes(self, testdir): ] ) - def test_docstring_partial_context_around_error(self, testdir): + def test_docstring_partial_context_around_error(self, pytester: Pytester): """Test that we show some context before the actual line of a failing doctest. """ - testdir.makepyfile( + pytester.makepyfile( ''' def foo(): """ @@ -258,7 +262,7 @@ def foo(): """ ''' ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*docstring_partial_context_around_error*", @@ -276,11 +280,11 @@ def foo(): result.stdout.no_fnmatch_line("*text-line-2*") result.stdout.no_fnmatch_line("*text-line-after*") - def test_docstring_full_context_around_error(self, testdir): + def test_docstring_full_context_around_error(self, pytester: Pytester): """Test that we show the whole context before the actual line of a failing doctest, provided that the context is up to 10 lines long. """ - testdir.makepyfile( + pytester.makepyfile( ''' def foo(): """ @@ -292,7 +296,7 @@ def foo(): """ ''' ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*docstring_full_context_around_error*", @@ -306,8 +310,8 @@ def foo(): ] ) - def test_doctest_linedata_missing(self, testdir): - testdir.tmpdir.join("hello.py").write( + def test_doctest_linedata_missing(self, pytester: Pytester): + pytester.path.joinpath("hello.py").write_text( textwrap.dedent( """\ class Fun(object): @@ -320,13 +324,13 @@ def test(self): """ ) ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( ["*hello*", "006*>>> 1/0*", "*UNEXPECTED*ZeroDivision*", "*1 failed*"] ) - def test_doctest_linedata_on_property(self, testdir): - testdir.makepyfile( + def test_doctest_linedata_on_property(self, pytester: Pytester): + pytester.makepyfile( """ class Sample(object): @property @@ -338,7 +342,7 @@ def some_property(self): return 'something' """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*= FAILURES =*", @@ -355,8 +359,8 @@ def some_property(self): ] ) - def test_doctest_no_linedata_on_overriden_property(self, testdir): - testdir.makepyfile( + def test_doctest_no_linedata_on_overriden_property(self, pytester: Pytester): + pytester.makepyfile( """ class Sample(object): @property @@ -369,7 +373,7 @@ def some_property(self): some_property = property(some_property.__get__, None, None, some_property.__doc__) """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*= FAILURES =*", @@ -386,14 +390,14 @@ def some_property(self): ] ) - def test_doctest_unex_importerror_only_txt(self, testdir): - testdir.maketxtfile( + def test_doctest_unex_importerror_only_txt(self, pytester: Pytester): + pytester.maketxtfile( """ >>> import asdalsdkjaslkdjasd >>> """ ) - result = testdir.runpytest() + result = pytester.runpytest() # doctest is never executed because of error during hello.py collection result.stdout.fnmatch_lines( [ @@ -403,21 +407,21 @@ def test_doctest_unex_importerror_only_txt(self, testdir): ] ) - def test_doctest_unex_importerror_with_module(self, testdir): - testdir.tmpdir.join("hello.py").write( + def test_doctest_unex_importerror_with_module(self, pytester: Pytester): + pytester.path.joinpath("hello.py").write_text( textwrap.dedent( """\ import asdalsdkjaslkdjasd """ ) ) - testdir.maketxtfile( + pytester.maketxtfile( """ >>> import hello >>> """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") # doctest is never executed because of error during hello.py collection result.stdout.fnmatch_lines( [ @@ -427,8 +431,8 @@ def test_doctest_unex_importerror_with_module(self, testdir): ] ) - def test_doctestmodule(self, testdir): - p = testdir.makepyfile( + def test_doctestmodule(self, pytester: Pytester): + p = pytester.makepyfile( """ ''' >>> x = 1 @@ -438,12 +442,12 @@ def test_doctestmodule(self, testdir): ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1) - def test_doctestmodule_external_and_issue116(self, testdir): - p = testdir.mkpydir("hello") - p.join("__init__.py").write( + def test_doctestmodule_external_and_issue116(self, pytester: Pytester): + p = pytester.mkpydir("hello") + p.joinpath("__init__.py").write_text( textwrap.dedent( """\ def somefunc(): @@ -455,7 +459,7 @@ def somefunc(): """ ) ) - result = testdir.runpytest(p, "--doctest-modules") + result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines( [ "003 *>>> i = 0", @@ -468,15 +472,15 @@ def somefunc(): ] ) - def test_txtfile_failing(self, testdir): - p = testdir.maketxtfile( + def test_txtfile_failing(self, pytester: Pytester): + p = pytester.maketxtfile( """ >>> i = 0 >>> i + 1 2 """ ) - result = testdir.runpytest(p, "-s") + result = pytester.runpytest(p, "-s") result.stdout.fnmatch_lines( [ "001 >>> i = 0", @@ -489,25 +493,25 @@ def test_txtfile_failing(self, testdir): ] ) - def test_txtfile_with_fixtures(self, testdir): - p = testdir.maketxtfile( + def test_txtfile_with_fixtures(self, pytester: Pytester): + p = pytester.maketxtfile( """ - >>> dir = getfixture('tmpdir') - >>> type(dir).__name__ - 'LocalPath' + >>> p = getfixture('tmp_path') + >>> p.is_dir() + True """ ) - reprec = testdir.inline_run(p) + reprec = pytester.inline_run(p) reprec.assertoutcome(passed=1) - def test_txtfile_with_usefixtures_in_ini(self, testdir): - testdir.makeini( + def test_txtfile_with_usefixtures_in_ini(self, pytester: Pytester): + pytester.makeini( """ [pytest] usefixtures = myfixture """ ) - testdir.makeconftest( + pytester.makeconftest( """ import pytest @pytest.fixture @@ -516,36 +520,36 @@ def myfixture(monkeypatch): """ ) - p = testdir.maketxtfile( + p = pytester.maketxtfile( """ >>> import os >>> os.environ["HELLO"] 'WORLD' """ ) - reprec = testdir.inline_run(p) + reprec = pytester.inline_run(p) reprec.assertoutcome(passed=1) - def test_doctestmodule_with_fixtures(self, testdir): - p = testdir.makepyfile( + def test_doctestmodule_with_fixtures(self, pytester: Pytester): + p = pytester.makepyfile( """ ''' - >>> dir = getfixture('tmpdir') - >>> type(dir).__name__ - 'LocalPath' + >>> p = getfixture('tmp_path') + >>> p.is_dir() + True ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) - def test_doctestmodule_three_tests(self, testdir): - p = testdir.makepyfile( + def test_doctestmodule_three_tests(self, pytester: Pytester): + p = pytester.makepyfile( """ ''' - >>> dir = getfixture('tmpdir') - >>> type(dir).__name__ - 'LocalPath' + >>> p = getfixture('tmp_path') + >>> p.is_dir() + True ''' def my_func(): ''' @@ -563,11 +567,11 @@ def another(): ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=3) - def test_doctestmodule_two_tests_one_fail(self, testdir): - p = testdir.makepyfile( + def test_doctestmodule_two_tests_one_fail(self, pytester: Pytester): + p = pytester.makepyfile( """ class MyClass(object): def bad_meth(self): @@ -584,17 +588,17 @@ def nice_meth(self): ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1, passed=1) - def test_ignored_whitespace(self, testdir): - testdir.makeini( + def test_ignored_whitespace(self, pytester: Pytester): + pytester.makeini( """ [pytest] doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE """ ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ class MyClass(object): ''' @@ -605,17 +609,17 @@ class MyClass(object): pass """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) - def test_non_ignored_whitespace(self, testdir): - testdir.makeini( + def test_non_ignored_whitespace(self, pytester: Pytester): + pytester.makeini( """ [pytest] doctest_optionflags = ELLIPSIS """ ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ class MyClass(object): ''' @@ -626,46 +630,46 @@ class MyClass(object): pass """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1, passed=0) - def test_ignored_whitespace_glob(self, testdir): - testdir.makeini( + def test_ignored_whitespace_glob(self, pytester: Pytester): + pytester.makeini( """ [pytest] doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE """ ) - p = testdir.maketxtfile( + p = pytester.maketxtfile( xdoc=""" >>> a = "foo " >>> print(a) foo """ ) - reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") + reprec = pytester.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(passed=1) - def test_non_ignored_whitespace_glob(self, testdir): - testdir.makeini( + def test_non_ignored_whitespace_glob(self, pytester: Pytester): + pytester.makeini( """ [pytest] doctest_optionflags = ELLIPSIS """ ) - p = testdir.maketxtfile( + p = pytester.maketxtfile( xdoc=""" >>> a = "foo " >>> print(a) foo """ ) - reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") + reprec = pytester.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(failed=1, passed=0) - def test_contains_unicode(self, testdir): + def test_contains_unicode(self, pytester: Pytester): """Fix internal error with docstrings containing non-ascii characters.""" - testdir.makepyfile( + pytester.makepyfile( '''\ def foo(): """ @@ -674,11 +678,11 @@ def foo(): """ ''' ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["Got nothing", "* 1 failed in*"]) - def test_ignore_import_errors_on_doctest(self, testdir): - p = testdir.makepyfile( + def test_ignore_import_errors_on_doctest(self, pytester: Pytester): + p = pytester.makepyfile( """ import asdf @@ -691,14 +695,14 @@ def add_one(x): """ ) - reprec = testdir.inline_run( + reprec = pytester.inline_run( p, "--doctest-modules", "--doctest-ignore-import-errors" ) reprec.assertoutcome(skipped=1, failed=1, passed=0) - def test_junit_report_for_doctest(self, testdir): + def test_junit_report_for_doctest(self, pytester: Pytester): """#713: Fix --junit-xml option when used with --doctest-modules.""" - p = testdir.makepyfile( + p = pytester.makepyfile( """ def foo(): ''' @@ -708,15 +712,15 @@ def foo(): pass """ ) - reprec = testdir.inline_run(p, "--doctest-modules", "--junit-xml=junit.xml") + reprec = pytester.inline_run(p, "--doctest-modules", "--junit-xml=junit.xml") reprec.assertoutcome(failed=1) - def test_unicode_doctest(self, testdir): + def test_unicode_doctest(self, pytester: Pytester): """ Test case for issue 2434: DecodeError on Python 2 when doctest contains non-ascii characters. """ - p = testdir.maketxtfile( + p = pytester.maketxtfile( test_unicode_doctest=""" .. doctest:: @@ -729,17 +733,17 @@ def test_unicode_doctest(self, testdir): 1 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines( ["*UNEXPECTED EXCEPTION: ZeroDivisionError*", "*1 failed*"] ) - def test_unicode_doctest_module(self, testdir): + def test_unicode_doctest_module(self, pytester: Pytester): """ Test case for issue 2434: DecodeError on Python 2 when doctest docstring contains non-ascii characters. """ - p = testdir.makepyfile( + p = pytester.makepyfile( test_unicode_doctest_module=""" def fix_bad_unicode(text): ''' @@ -749,15 +753,15 @@ def fix_bad_unicode(text): return "único" """ ) - result = testdir.runpytest(p, "--doctest-modules") + result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) - def test_print_unicode_value(self, testdir): + def test_print_unicode_value(self, pytester: Pytester): """ Test case for issue 3583: Printing Unicode in doctest under Python 2.7 doesn't work """ - p = testdir.maketxtfile( + p = pytester.maketxtfile( test_print_unicode_value=r""" Here is a doctest:: @@ -765,12 +769,12 @@ def test_print_unicode_value(self, testdir): åéîøü """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["* 1 passed *"]) - def test_reportinfo(self, testdir): + def test_reportinfo(self, pytester: Pytester): """Make sure that DoctestItem.reportinfo() returns lineno.""" - p = testdir.makepyfile( + p = pytester.makepyfile( test_reportinfo=""" def foo(x): ''' @@ -780,16 +784,16 @@ def foo(x): return 'c' """ ) - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + items, reprec = pytester.inline_genitems(p, "--doctest-modules") reportinfo = items[0].reportinfo() assert reportinfo[1] == 1 - def test_valid_setup_py(self, testdir): + def test_valid_setup_py(self, pytester: Pytester): """ Test to make sure that pytest ignores valid setup.py files when ran with --doctest-modules """ - p = testdir.makepyfile( + p = pytester.makepyfile( setup=""" from setuptools import setup, find_packages setup(name='sample', @@ -799,33 +803,33 @@ def test_valid_setup_py(self, testdir): ) """ ) - result = testdir.runpytest(p, "--doctest-modules") + result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["*collected 0 items*"]) - def test_invalid_setup_py(self, testdir): + def test_invalid_setup_py(self, pytester: Pytester): """ Test to make sure that pytest reads setup.py files that are not used for python packages when ran with --doctest-modules """ - p = testdir.makepyfile( + p = pytester.makepyfile( setup=""" def test_foo(): return 'bar' """ ) - result = testdir.runpytest(p, "--doctest-modules") + result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["*collected 1 item*"]) class TestLiterals: @pytest.mark.parametrize("config_mode", ["ini", "comment"]) - def test_allow_unicode(self, testdir, config_mode): + def test_allow_unicode(self, pytester, config_mode): """Test that doctests which output unicode work in all python versions tested by pytest when the ALLOW_UNICODE option is used (either in the ini file or by an inline comment). """ if config_mode == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] doctest_optionflags = ALLOW_UNICODE @@ -835,7 +839,7 @@ def test_allow_unicode(self, testdir, config_mode): else: comment = "#doctest: +ALLOW_UNICODE" - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> b'12'.decode('ascii') {comment} '12' @@ -843,7 +847,7 @@ def test_allow_unicode(self, testdir, config_mode): comment=comment ) ) - testdir.makepyfile( + pytester.makepyfile( foo=""" def foo(): ''' @@ -854,17 +858,17 @@ def foo(): comment=comment ) ) - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=2) @pytest.mark.parametrize("config_mode", ["ini", "comment"]) - def test_allow_bytes(self, testdir, config_mode): + def test_allow_bytes(self, pytester, config_mode): """Test that doctests which output bytes work in all python versions tested by pytest when the ALLOW_BYTES option is used (either in the ini file or by an inline comment)(#1287). """ if config_mode == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] doctest_optionflags = ALLOW_BYTES @@ -874,7 +878,7 @@ def test_allow_bytes(self, testdir, config_mode): else: comment = "#doctest: +ALLOW_BYTES" - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> b'foo' {comment} 'foo' @@ -882,7 +886,7 @@ def test_allow_bytes(self, testdir, config_mode): comment=comment ) ) - testdir.makepyfile( + pytester.makepyfile( foo=""" def foo(): ''' @@ -893,34 +897,34 @@ def foo(): comment=comment ) ) - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=2) - def test_unicode_string(self, testdir): + def test_unicode_string(self, pytester: Pytester): """Test that doctests which output unicode fail in Python 2 when the ALLOW_UNICODE option is not used. The same test should pass in Python 3. """ - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> b'12'.decode('ascii') '12' """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_bytes_literal(self, testdir): + def test_bytes_literal(self, pytester: Pytester): """Test that doctests which output bytes fail in Python 3 when the ALLOW_BYTES option is not used. (#1287). """ - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> b'foo' 'foo' """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(failed=1) def test_number_re(self) -> None: @@ -954,10 +958,10 @@ def test_number_re(self) -> None: assert _number_re.match(s) is None @pytest.mark.parametrize("config_mode", ["ini", "comment"]) - def test_number_precision(self, testdir, config_mode): + def test_number_precision(self, pytester, config_mode): """Test the NUMBER option.""" if config_mode == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] doctest_optionflags = NUMBER @@ -967,7 +971,7 @@ def test_number_precision(self, testdir, config_mode): else: comment = "#doctest: +NUMBER" - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" Scalars: @@ -1024,7 +1028,7 @@ def test_number_precision(self, testdir, config_mode): comment=comment ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @pytest.mark.parametrize( @@ -1048,8 +1052,8 @@ def test_number_precision(self, testdir, config_mode): pytest.param("'3.1416'", "'3.14'", marks=pytest.mark.xfail), # type: ignore ], ) - def test_number_non_matches(self, testdir, expression, output): - testdir.maketxtfile( + def test_number_non_matches(self, pytester, expression, output): + pytester.maketxtfile( test_doc=""" >>> {expression} #doctest: +NUMBER {output} @@ -1057,11 +1061,11 @@ def test_number_non_matches(self, testdir, expression, output): expression=expression, output=output ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=0, failed=1) - def test_number_and_allow_unicode(self, testdir): - testdir.maketxtfile( + def test_number_and_allow_unicode(self, pytester: Pytester): + pytester.maketxtfile( test_doc=""" >>> from collections import namedtuple >>> T = namedtuple('T', 'a b c') @@ -1069,7 +1073,7 @@ def test_number_and_allow_unicode(self, testdir): T(a=0.233, b=u'str', c='bytes') """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -1080,18 +1084,18 @@ class TestDoctestSkips: """ @pytest.fixture(params=["text", "module"]) - def makedoctest(self, testdir, request): + def makedoctest(self, pytester, request): def makeit(doctest): mode = request.param if mode == "text": - testdir.maketxtfile(doctest) + pytester.maketxtfile(doctest) else: assert mode == "module" - testdir.makepyfile('"""\n%s"""' % doctest) + pytester.makepyfile('"""\n%s"""' % doctest) return makeit - def test_one_skipped(self, testdir, makedoctest): + def test_one_skipped(self, pytester, makedoctest): makedoctest( """ >>> 1 + 1 # doctest: +SKIP @@ -1100,10 +1104,10 @@ def test_one_skipped(self, testdir, makedoctest): 4 """ ) - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=1) - def test_one_skipped_failed(self, testdir, makedoctest): + def test_one_skipped_failed(self, pytester, makedoctest): makedoctest( """ >>> 1 + 1 # doctest: +SKIP @@ -1112,10 +1116,10 @@ def test_one_skipped_failed(self, testdir, makedoctest): 200 """ ) - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(failed=1) - def test_all_skipped(self, testdir, makedoctest): + def test_all_skipped(self, pytester, makedoctest): makedoctest( """ >>> 1 + 1 # doctest: +SKIP @@ -1124,16 +1128,16 @@ def test_all_skipped(self, testdir, makedoctest): 200 """ ) - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(skipped=1) - def test_vacuous_all_skipped(self, testdir, makedoctest): + def test_vacuous_all_skipped(self, pytester, makedoctest): makedoctest("") - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=0, skipped=0) - def test_continue_on_failure(self, testdir): - testdir.maketxtfile( + def test_continue_on_failure(self, pytester: Pytester): + pytester.maketxtfile( test_something=""" >>> i = 5 >>> def foo(): @@ -1145,7 +1149,9 @@ def test_continue_on_failure(self, testdir): >>> i + 1 """ ) - result = testdir.runpytest("--doctest-modules", "--doctest-continue-on-failure") + result = pytester.runpytest( + "--doctest-modules", "--doctest-continue-on-failure" + ) result.assert_outcomes(passed=0, failed=1) # The lines that contains the failure are 4, 5, and 8. The first one # is a stack trace and the other two are mismatches. @@ -1158,11 +1164,11 @@ class TestDoctestAutoUseFixtures: SCOPES = ["module", "session", "class", "function"] - def test_doctest_module_session_fixture(self, testdir): + def test_doctest_module_session_fixture(self, pytester: Pytester): """Test that session fixtures are initialized for doctest modules (#768).""" # session fixture which changes some global data, which will # be accessed by doctests in a module - testdir.makeconftest( + pytester.makeconftest( """ import pytest import sys @@ -1175,7 +1181,7 @@ def myfixture(): del sys.pytest_session_data """ ) - testdir.makepyfile( + pytester.makepyfile( foo=""" import sys @@ -1190,16 +1196,16 @@ def bar(): ''' """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["*2 passed*"]) @pytest.mark.parametrize("scope", SCOPES) @pytest.mark.parametrize("enable_doctest", [True, False]) - def test_fixture_scopes(self, testdir, scope, enable_doctest): + def test_fixture_scopes(self, pytester, scope, enable_doctest): """Test that auto-use fixtures work properly with doctest modules. See #1057 and #1100. """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -1210,7 +1216,7 @@ def auto(request): scope=scope ) ) - testdir.makepyfile( + pytester.makepyfile( test_1=''' def test_foo(): """ @@ -1223,19 +1229,19 @@ def test_bar(): ) params = ("--doctest-modules",) if enable_doctest else () passes = 3 if enable_doctest else 2 - result = testdir.runpytest(*params) + result = pytester.runpytest(*params) result.stdout.fnmatch_lines(["*=== %d passed in *" % passes]) @pytest.mark.parametrize("scope", SCOPES) @pytest.mark.parametrize("autouse", [True, False]) @pytest.mark.parametrize("use_fixture_in_doctest", [True, False]) def test_fixture_module_doctest_scopes( - self, testdir, scope, autouse, use_fixture_in_doctest + self, pytester, scope, autouse, use_fixture_in_doctest ): """Test that auto-use fixtures work properly with doctest files. See #1057 and #1100. """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -1247,29 +1253,29 @@ def auto(request): ) ) if use_fixture_in_doctest: - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> getfixture('auto') 99 """ ) else: - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> 1 + 1 2 """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.no_fnmatch_line("*FAILURES*") result.stdout.fnmatch_lines(["*=== 1 passed in *"]) @pytest.mark.parametrize("scope", SCOPES) - def test_auto_use_request_attributes(self, testdir, scope): + def test_auto_use_request_attributes(self, pytester, scope): """Check that all attributes of a request in an autouse fixture behave as expected when requested for a doctest item. """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -1286,13 +1292,13 @@ def auto(request): scope=scope ) ) - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> 1 + 1 2 """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") str(result.stdout.no_fnmatch_line("*FAILURES*")) result.stdout.fnmatch_lines(["*=== 1 passed in *"]) @@ -1302,12 +1308,12 @@ class TestDoctestNamespaceFixture: SCOPES = ["module", "session", "class", "function"] @pytest.mark.parametrize("scope", SCOPES) - def test_namespace_doctestfile(self, testdir, scope): + def test_namespace_doctestfile(self, pytester, scope): """ Check that inserting something into the namespace works in a simple text file doctest """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest import contextlib @@ -1319,22 +1325,22 @@ def add_contextlib(doctest_namespace): scope=scope ) ) - p = testdir.maketxtfile( + p = pytester.maketxtfile( """ >>> print(cl.__name__) contextlib """ ) - reprec = testdir.inline_run(p) + reprec = pytester.inline_run(p) reprec.assertoutcome(passed=1) @pytest.mark.parametrize("scope", SCOPES) - def test_namespace_pyfile(self, testdir, scope): + def test_namespace_pyfile(self, pytester, scope): """ Check that inserting something into the namespace works in a simple Python file docstring doctest """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest import contextlib @@ -1346,7 +1352,7 @@ def add_contextlib(doctest_namespace): scope=scope ) ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ def foo(): ''' @@ -1355,13 +1361,13 @@ def foo(): ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) class TestDoctestReportingOption: - def _run_doctest_report(self, testdir, format): - testdir.makepyfile( + def _run_doctest_report(self, pytester, format): + pytester.makepyfile( """ def foo(): ''' @@ -1377,17 +1383,17 @@ def foo(): '2 3 6') """ ) - return testdir.runpytest("--doctest-modules", "--doctest-report", format) + return pytester.runpytest("--doctest-modules", "--doctest-report", format) @pytest.mark.parametrize("format", ["udiff", "UDIFF", "uDiFf"]) - def test_doctest_report_udiff(self, testdir, format): - result = self._run_doctest_report(testdir, format) + def test_doctest_report_udiff(self, pytester, format): + result = self._run_doctest_report(pytester, format) result.stdout.fnmatch_lines( [" 0 1 4", " -1 2 4", " +1 2 5", " 2 3 6"] ) - def test_doctest_report_cdiff(self, testdir): - result = self._run_doctest_report(testdir, "cdiff") + def test_doctest_report_cdiff(self, pytester: Pytester): + result = self._run_doctest_report(pytester, "cdiff") result.stdout.fnmatch_lines( [ " a b", @@ -1402,8 +1408,8 @@ def test_doctest_report_cdiff(self, testdir): ] ) - def test_doctest_report_ndiff(self, testdir): - result = self._run_doctest_report(testdir, "ndiff") + def test_doctest_report_ndiff(self, pytester: Pytester): + result = self._run_doctest_report(pytester, "ndiff") result.stdout.fnmatch_lines( [ " a b", @@ -1417,8 +1423,8 @@ def test_doctest_report_ndiff(self, testdir): ) @pytest.mark.parametrize("format", ["none", "only_first_failure"]) - def test_doctest_report_none_or_only_first_failure(self, testdir, format): - result = self._run_doctest_report(testdir, format) + def test_doctest_report_none_or_only_first_failure(self, pytester, format): + result = self._run_doctest_report(pytester, format) result.stdout.fnmatch_lines( [ "Expected:", @@ -1434,8 +1440,8 @@ def test_doctest_report_none_or_only_first_failure(self, testdir, format): ] ) - def test_doctest_report_invalid(self, testdir): - result = self._run_doctest_report(testdir, "obviously_invalid_format") + def test_doctest_report_invalid(self, pytester: Pytester): + result = self._run_doctest_report(pytester, "obviously_invalid_format") result.stderr.fnmatch_lines( [ "*error: argument --doctest-report: invalid choice: 'obviously_invalid_format' (choose from*" @@ -1444,9 +1450,9 @@ def test_doctest_report_invalid(self, testdir): @pytest.mark.parametrize("mock_module", ["mock", "unittest.mock"]) -def test_doctest_mock_objects_dont_recurse_missbehaved(mock_module, testdir): +def test_doctest_mock_objects_dont_recurse_missbehaved(mock_module, pytester: Pytester): pytest.importorskip(mock_module) - testdir.makepyfile( + pytester.makepyfile( """ from {mock_module} import call class Example(object): @@ -1458,7 +1464,7 @@ class Example(object): mock_module=mock_module ) ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) @@ -1485,10 +1491,10 @@ def test_warning_on_unwrap_of_broken_object( assert inspect.unwrap.__module__ == "inspect" -def test_is_setup_py_not_named_setup_py(tmpdir): - not_setup_py = tmpdir.join("not_setup.py") - not_setup_py.write('from setuptools import setup; setup(name="foo")') - assert not _is_setup_py(not_setup_py) +def test_is_setup_py_not_named_setup_py(tmp_path): + not_setup_py = tmp_path.joinpath("not_setup.py") + not_setup_py.write_text('from setuptools import setup; setup(name="foo")') + assert not _is_setup_py(py.path.local(str(not_setup_py))) @pytest.mark.parametrize("mod", ("setuptools", "distutils.core")) @@ -1499,11 +1505,11 @@ def test_is_setup_py_is_a_setup_py(tmpdir, mod): @pytest.mark.parametrize("mod", ("setuptools", "distutils.core")) -def test_is_setup_py_different_encoding(tmpdir, mod): - setup_py = tmpdir.join("setup.py") +def test_is_setup_py_different_encoding(tmp_path, mod): + setup_py = tmp_path.joinpath("setup.py") contents = ( "# -*- coding: cp1252 -*-\n" 'from {} import setup; setup(name="foo", description="€")\n'.format(mod) ) - setup_py.write_binary(contents.encode("cp1252")) - assert _is_setup_py(setup_py) + setup_py.write_bytes(contents.encode("cp1252")) + assert _is_setup_py(py.path.local(str(setup_py))) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index dd3855c69ff..fed201dafe5 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -801,9 +801,10 @@ def test_parse_summary_line_always_plural(): def test_makefile_joins_absolute_path(testdir: Testdir) -> None: absfile = testdir.tmpdir / "absfile" - if sys.platform == "win32": - with pytest.raises(OSError): - testdir.makepyfile(**{str(absfile): ""}) - else: - p1 = testdir.makepyfile(**{str(absfile): ""}) - assert str(p1) == (testdir.tmpdir / absfile) + ".py" + p1 = testdir.makepyfile(**{str(absfile): ""}) + assert str(p1) == str(testdir.tmpdir / "absfile.py") + + +def test_testtmproot(testdir): + """Check test_tmproot is a py.path attribute for backward compatibility.""" + assert testdir.test_tmproot.check(dir=1) From 3cae145e4171a92be6f1a97f888e9f2bf0bc364d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 13 Oct 2020 12:01:11 -0300 Subject: [PATCH 693/823] List Testdir members in the docs Also include docstrings pointing to the counterparts in Pytester. Fix #7892 --- doc/en/reference.rst | 7 +++++++ src/_pytest/pytester.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 6795b721c8f..667547894c4 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -530,6 +530,9 @@ To use it, include in your topmost ``conftest.py`` file: .. autoclass:: LineMatcher() :members: +.. autoclass:: HookRecorder() + :members: + .. fixture:: testdir testdir @@ -540,6 +543,10 @@ legacy ``py.path.local`` objects instead when applicable. New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. +.. autoclass:: Testdir() + :members: + + .. fixture:: recwarn recwarn diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index b7a79b90299..e3094a9df9c 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1494,6 +1494,7 @@ class Testdir: @property def tmpdir(self) -> py.path.local: + """Temporary directory where tests are executed.""" return py.path.local(self._pytester.path) @property @@ -1517,89 +1518,117 @@ def monkeypatch(self) -> MonkeyPatch: return self._pytester._monkeypatch def make_hook_recorder(self, pluginmanager) -> HookRecorder: + """See :meth:`Pytester.make_hook_recorder`.""" return self._pytester.make_hook_recorder(pluginmanager) def chdir(self) -> None: + """See :meth:`Pytester.chdir`.""" return self._pytester.chdir() def finalize(self) -> None: + """See :meth:`Pytester._finalize`.""" return self._pytester._finalize() def makefile(self, ext, *args, **kwargs) -> py.path.local: + """See :meth:`Pytester.makefile`.""" return py.path.local(str(self._pytester.makefile(ext, *args, **kwargs))) def makeconftest(self, source) -> py.path.local: + """See :meth:`Pytester.makeconftest`.""" return py.path.local(str(self._pytester.makeconftest(source))) def makeini(self, source) -> py.path.local: + """See :meth:`Pytester.makeini`.""" return py.path.local(str(self._pytester.makeini(source))) def getinicfg(self, source) -> py.path.local: + """See :meth:`Pytester.getinicfg`.""" return py.path.local(str(self._pytester.getinicfg(source))) def makepyprojecttoml(self, source) -> py.path.local: + """See :meth:`Pytester.makepyprojecttoml`.""" return py.path.local(str(self._pytester.makepyprojecttoml(source))) def makepyfile(self, *args, **kwargs) -> py.path.local: + """See :meth:`Pytester.makepyfile`.""" return py.path.local(str(self._pytester.makepyfile(*args, **kwargs))) def maketxtfile(self, *args, **kwargs) -> py.path.local: + """See :meth:`Pytester.maketxtfile`.""" return py.path.local(str(self._pytester.maketxtfile(*args, **kwargs))) def syspathinsert(self, path=None) -> None: + """See :meth:`Pytester.syspathinsert`.""" return self._pytester.syspathinsert(path) def mkdir(self, name) -> py.path.local: + """See :meth:`Pytester.mkdir`.""" return py.path.local(str(self._pytester.mkdir(name))) def mkpydir(self, name) -> py.path.local: + """See :meth:`Pytester.mkpydir`.""" return py.path.local(str(self._pytester.mkpydir(name))) def copy_example(self, name=None) -> py.path.local: + """See :meth:`Pytester.copy_example`.""" return py.path.local(str(self._pytester.copy_example(name))) def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]: + """See :meth:`Pytester.getnode`.""" return self._pytester.getnode(config, arg) def getpathnode(self, path): + """See :meth:`Pytester.getpathnode`.""" return self._pytester.getpathnode(path) def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]: + """See :meth:`Pytester.genitems`.""" return self._pytester.genitems(colitems) def runitem(self, source): + """See :meth:`Pytester.runitem`.""" return self._pytester.runitem(source) def inline_runsource(self, source, *cmdlineargs): + """See :meth:`Pytester.inline_runsource`.""" return self._pytester.inline_runsource(source, *cmdlineargs) def inline_genitems(self, *args): + """See :meth:`Pytester.inline_genitems`.""" return self._pytester.inline_genitems(*args) def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): + """See :meth:`Pytester.inline_run`.""" return self._pytester.inline_run( *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc ) def runpytest_inprocess(self, *args, **kwargs) -> RunResult: + """See :meth:`Pytester.runpytest_inprocess`.""" return self._pytester.runpytest_inprocess(*args, **kwargs) def runpytest(self, *args, **kwargs) -> RunResult: + """See :meth:`Pytester.runpytest`.""" return self._pytester.runpytest(*args, **kwargs) def parseconfig(self, *args) -> Config: + """See :meth:`Pytester.parseconfig`.""" return self._pytester.parseconfig(*args) def parseconfigure(self, *args) -> Config: + """See :meth:`Pytester.parseconfigure`.""" return self._pytester.parseconfigure(*args) def getitem(self, source, funcname="test_func"): + """See :meth:`Pytester.getitem`.""" return self._pytester.getitem(source, funcname) def getitems(self, source): + """See :meth:`Pytester.getitems`.""" return self._pytester.getitems(source) def getmodulecol(self, source, configargs=(), withinit=False): + """See :meth:`Pytester.getmodulecol`.""" return self._pytester.getmodulecol( source, configargs=configargs, withinit=withinit ) @@ -1607,6 +1636,7 @@ def getmodulecol(self, source, configargs=(), withinit=False): def collect_by_name( self, modcol: Module, name: str ) -> Optional[Union[Item, Collector]]: + """See :meth:`Pytester.collect_by_name`.""" return self._pytester.collect_by_name(modcol, name) def popen( @@ -1617,26 +1647,33 @@ def popen( stdin=CLOSE_STDIN, **kw, ): + """See :meth:`Pytester.popen`.""" return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw) def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: + """See :meth:`Pytester.run`.""" return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin) def runpython(self, script) -> RunResult: + """See :meth:`Pytester.runpython`.""" return self._pytester.runpython(script) def runpython_c(self, command): + """See :meth:`Pytester.runpython_c`.""" return self._pytester.runpython_c(command) def runpytest_subprocess(self, *args, timeout=None) -> RunResult: + """See :meth:`Pytester.runpytest_subprocess`.""" return self._pytester.runpytest_subprocess(*args, timeout=timeout) def spawn_pytest( self, string: str, expect_timeout: float = 10.0 ) -> "pexpect.spawn": + """See :meth:`Pytester.spawn_pytest`.""" return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout) def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": + """See :meth:`Pytester.spawn`.""" return self._pytester.spawn(cmd, expect_timeout=expect_timeout) def __repr__(self) -> str: From 5182c73feae59a86d9b2345b059ff9013e21a2d7 Mon Sep 17 00:00:00 2001 From: Prashant Sharma <31796326+gutsytechster@users.noreply.github.com> Date: Wed, 14 Oct 2020 17:47:50 +0530 Subject: [PATCH 694/823] Add example for registering multiple custom mark (#7886) --- AUTHORS | 1 + doc/en/example/markers.rst | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 37c1e3c062b..d2bb3d3ec0f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -233,6 +233,7 @@ Pieter Mulder Piotr Banaszkiewicz Piotr Helm Prashant Anand +Prashant Sharma Pulkit Goyal Punyashloka Biswal Quentin Pradet diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 3d55f9ebb04..1dc44be3404 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -221,14 +221,19 @@ Registering markers for your test suite is simple: [pytest] markers = webtest: mark a test as a webtest. + slow: mark test as slow. -You can ask which markers exist for your test suite - the list includes our just defined ``webtest`` markers: +Multiple custom markers can be registered, by defining each one in its own line, as shown in above example. + +You can ask which markers exist for your test suite - the list includes our just defined ``webtest`` and ``slow`` markers: .. code-block:: pytest $ pytest --markers @pytest.mark.webtest: mark a test as a webtest. + @pytest.mark.slow: mark test as slow. + @pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/stable/warnings.html#pytest-mark-filterwarnings @pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test. From fc70fd23a2edb2ba71b070f1160a4423cb0d8094 Mon Sep 17 00:00:00 2001 From: kwgchi Date: Fri, 16 Oct 2020 21:29:58 +0900 Subject: [PATCH 695/823] Fix typos --- doc/en/backwards-compatibility.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/backwards-compatibility.rst b/doc/en/backwards-compatibility.rst index d5b2d79d633..7b3027e790a 100644 --- a/doc/en/backwards-compatibility.rst +++ b/doc/en/backwards-compatibility.rst @@ -10,7 +10,7 @@ we keep learning about new and better structures to express different details ab While we implement those modifications we try to ensure an easy transition and don't want to impose unnecessary churn on our users and community/plugin authors. -As of now, pytest considers multipe types of backward compatibility transitions: +As of now, pytest considers multiple types of backward compatibility transitions: a) trivial: APIs which trivially translate to the new mechanism, and do not cause problematic changes. @@ -25,7 +25,7 @@ b) transitional: the old and new API don't conflict When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn them into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed. -c) true breakage: should only to be considered when normal transition is unreasonably unsustainable and would offset important development/features by years. +c) true breakage: should only be considered when normal transition is unreasonably unsustainable and would offset important development/features by years. In addition, they should be limited to APIs where the number of actual users is very small (for example only impacting some plugins), and can be coordinated with the community in advance. Examples for such upcoming changes: @@ -42,7 +42,7 @@ c) true breakage: should only to be considered when normal transition is unreaso After there's no hard *-1* on the issue it should be followed up by an initial proof-of-concept Pull Request. - This POC serves as both a coordination point to assess impact and potential inspriation to come up with a transitional solution after all. + This POC serves as both a coordination point to assess impact and potential inspiration to come up with a transitional solution after all. After a reasonable amount of time the PR can be merged to base a new major release. From 991bc7bd50772b0ae1f40b5f821f7e67745d1b2e Mon Sep 17 00:00:00 2001 From: Nimesh Vashistha <43320077+nimeshvashistha@users.noreply.github.com> Date: Sat, 17 Oct 2020 16:56:30 +0530 Subject: [PATCH 696/823] Added note to writing_plugins.rst (#7896) Co-authored-by: Bruno Oliveira --- doc/en/reference.rst | 4 ++++ doc/en/writing_plugins.rst | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 667547894c4..6e94d4c49bd 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -688,6 +688,10 @@ items, delete or otherwise amend the test items: .. autofunction:: pytest_collection_modifyitems +.. note:: + If this hook is implemented in ``conftest.py`` files, it always receives all collected items, not only those + under the ``conftest.py`` where it is implemented. + .. autofunction:: pytest_collection_finish Test running (runtest) hooks diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 0492b5fcf7e..41967525b33 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -107,6 +107,10 @@ Here is how you might run it:: See also: :ref:`pythonpath`. +.. note:: + Some hooks should be implemented only in plugins or conftest.py files situated at the + tests root directory due to how pytest discovers plugins during startup, + see the documentation of each hook for details. Writing your own plugin ----------------------- From 0a258f534f9c295703daadd70b1c8e93e788730a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 17 Oct 2020 08:42:15 -0300 Subject: [PATCH 697/823] Fix linting --- doc/en/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 6e94d4c49bd..5f0a20dc746 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -691,7 +691,7 @@ items, delete or otherwise amend the test items: .. note:: If this hook is implemented in ``conftest.py`` files, it always receives all collected items, not only those under the ``conftest.py`` where it is implemented. - + .. autofunction:: pytest_collection_finish Test running (runtest) hooks From e5e47c1097e6f9e7bd30e28d508dca489f0629c6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 17 Oct 2020 18:18:24 +0300 Subject: [PATCH 698/823] Fix typing related to iniconfig iniconfig now has typing stubs which reveal a couple issues. --- .pre-commit-config.yaml | 2 ++ src/_pytest/config/findpaths.py | 2 +- src/_pytest/pytester.py | 9 +++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75941dcd934..1574651c89c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,6 +54,8 @@ repos: - id: mypy files: ^(src/|testing/) args: [] + additional_dependencies: + - iniconfig>=1.1.0 - repo: local hooks: - id: rst diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 04fa8f37540..2edf54536ba 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -27,7 +27,7 @@ def _parse_ini_config(path: Path) -> iniconfig.IniConfig: Raise UsageError if the file cannot be parsed. """ try: - return iniconfig.IniConfig(path) + return iniconfig.IniConfig(str(path)) except iniconfig.ParseError as exc: raise UsageError(str(exc)) from exc diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index e3094a9df9c..a4142037d95 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -32,6 +32,7 @@ import attr import py from iniconfig import IniConfig +from iniconfig import SectionWrapper import pytest from _pytest import timing @@ -785,10 +786,10 @@ def makeini(self, source: str) -> Path: """Write a tox.ini file with 'source' as contents.""" return self.makefile(".ini", tox=source) - def getinicfg(self, source: str) -> IniConfig: + def getinicfg(self, source: str) -> SectionWrapper: """Return the pytest section from the tox.ini config file.""" p = self.makeini(source) - return IniConfig(p)["pytest"] + return IniConfig(str(p))["pytest"] def makepyprojecttoml(self, source: str) -> Path: """Write a pyproject.toml file with 'source' as contents. @@ -1541,9 +1542,9 @@ def makeini(self, source) -> py.path.local: """See :meth:`Pytester.makeini`.""" return py.path.local(str(self._pytester.makeini(source))) - def getinicfg(self, source) -> py.path.local: + def getinicfg(self, source: str) -> SectionWrapper: """See :meth:`Pytester.getinicfg`.""" - return py.path.local(str(self._pytester.getinicfg(source))) + return self._pytester.getinicfg(source) def makepyprojecttoml(self, source) -> py.path.local: """See :meth:`Pytester.makepyprojecttoml`.""" From 1b23a111d2056e56c4a9038bc272e6f1f1698f85 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 17 Oct 2020 16:54:54 +0300 Subject: [PATCH 699/823] Update mypy 0.782 -> 0.790 --- .pre-commit-config.yaml | 2 +- setup.cfg | 2 +- src/_pytest/cacheprovider.py | 2 +- src/_pytest/capture.py | 3 +-- src/_pytest/compat.py | 5 ++--- src/_pytest/pytester.py | 4 +++- src/_pytest/python_api.py | 8 ++++---- src/_pytest/runner.py | 2 +- testing/test_assertrewrite.py | 2 +- testing/test_capture.py | 2 +- testing/test_debugging.py | 7 +------ testing/test_doctest.py | 2 +- testing/test_pastebin.py | 3 ++- 13 files changed, 20 insertions(+), 24 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1574651c89c..2d351182eea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.782 # NOTE: keep this in sync with setup.cfg. + rev: v0.790 # NOTE: keep this in sync with setup.cfg. hooks: - id: mypy files: ^(src/|testing/) diff --git a/setup.cfg b/setup.cfg index 28ec061e028..134a80b3efd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,7 +63,7 @@ console_scripts = [options.extras_require] checkqa-mypy = - mypy==0.780 + mypy==0.790 testing = argcomplete hypothesis>=3.56 diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 2c8128f6175..20f0c71d339 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -382,7 +382,7 @@ def pytest_collection_modifyitems( self.cached_nodeids.update(item.nodeid for item in items) def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]: - return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) + return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) # type: ignore[no-any-return] def pytest_sessionfinish(self) -> None: config = self.config diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index dbb6d478f57..4d314998ce2 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -380,8 +380,7 @@ def __init__(self, targetfd: int) -> None: self.syscapture = SysCapture(targetfd) else: self.tmpfile = EncodedFile( - # TODO: Remove type ignore, fixed in next mypy release. - TemporaryFile(buffering=0), # type: ignore[arg-type] + TemporaryFile(buffering=0), encoding="utf-8", errors="replace", newline="", diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 0b6b1ca074c..c7f86ea9c0a 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -150,9 +150,8 @@ def getfuncargnames( p.name for p in parameters.values() if ( - # TODO: Remove type ignore after https://github.com/python/typeshed/pull/4383 - p.kind is Parameter.POSITIONAL_OR_KEYWORD # type: ignore[unreachable] - or p.kind is Parameter.KEYWORD_ONLY # type: ignore[unreachable] + p.kind is Parameter.POSITIONAL_OR_KEYWORD + or p.kind is Parameter.KEYWORD_ONLY ) and p.default is Parameter.empty ) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a4142037d95..dc234dc6356 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1322,8 +1322,10 @@ def run( """ __tracebackhide__ = True + # TODO: Remove type ignore in next mypy release. + # https://github.com/python/typeshed/pull/4582 cmdargs = tuple( - os.fspath(arg) if isinstance(arg, os.PathLike) else arg for arg in cmdargs + os.fspath(arg) if isinstance(arg, os.PathLike) else arg for arg in cmdargs # type: ignore[misc] ) p1 = self.path.joinpath("stdout") p2 = self.path.joinpath("stderr") diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index c4d029c0d5c..373435236ce 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -210,7 +210,7 @@ def __repr__(self) -> str: # tolerances, i.e. non-numerics and infinities. Need to call abs to # handle complex numbers, e.g. (inf + 1j). if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf( - abs(self.expected) + abs(self.expected) # type: ignore[arg-type] ): return str(self.expected) @@ -253,8 +253,8 @@ def __eq__(self, actual) -> bool: # Allow the user to control whether NaNs are considered equal to each # other or not. The abs() calls are for compatibility with complex # numbers. - if math.isnan(abs(self.expected)): - return self.nan_ok and math.isnan(abs(actual)) + if math.isnan(abs(self.expected)): # type: ignore[arg-type] + return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type] # Infinity shouldn't be approximately equal to anything but itself, but # if there's a relative tolerance, it will be infinite and infinity @@ -262,7 +262,7 @@ def __eq__(self, actual) -> bool: # case would have been short circuited above, so here we can just # return false if the expected value is infinite. The abs() call is # for compatibility with complex numbers. - if math.isinf(abs(self.expected)): + if math.isinf(abs(self.expected)): # type: ignore[arg-type] return False # Return true if the two numbers are within the tolerance. diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index cce9bdd9713..833d288f0de 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -77,7 +77,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: dlist.append(rep) if not dlist: return - dlist.sort(key=lambda x: x.duration) + dlist.sort(key=lambda x: x.duration) # type: ignore[no-any-return] dlist.reverse() if not durations: tr.write_sep("=", "slowest durations") diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 9f0aa31d132..f95fd54b3eb 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1595,7 +1595,7 @@ def test_get_cache_dir(self, monkeypatch, prefix, source, expected): if prefix: if sys.version_info < (3, 8): pytest.skip("pycache_prefix not available in py<38") - monkeypatch.setattr(sys, "pycache_prefix", prefix) # type:ignore + monkeypatch.setattr(sys, "pycache_prefix", prefix) assert get_cache_dir(Path(source)) == Path(expected) diff --git a/testing/test_capture.py b/testing/test_capture.py index 7aeb2d8acd9..e6bbc9a5d19 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1606,7 +1606,7 @@ def test_stderr_write_returns_len(capsys): def test_encodedfile_writelines(tmpfile: BinaryIO) -> None: ef = capture.EncodedFile(tmpfile, encoding="utf-8") with pytest.raises(TypeError): - ef.writelines([b"line1", b"line2"]) + ef.writelines([b"line1", b"line2"]) # type: ignore[list-item] assert ef.writelines(["line3", "line4"]) is None # type: ignore[func-returns-value] ef.flush() tmpfile.seek(0) diff --git a/testing/test_debugging.py b/testing/test_debugging.py index f22d5d724f2..2760930efb1 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -886,14 +886,9 @@ def test_foo(): class TestDebuggingBreakpoints: def test_supports_breakpoint_module_global(self): - """ - Test that supports breakpoint global marks on Python 3.7+ and not on - CPython 3.5, 2.7 - """ + """Test that supports breakpoint global marks on Python 3.7+.""" if sys.version_info >= (3, 7): assert SUPPORTS_BREAKPOINT_BUILTIN is True - if sys.version_info.major == 3 and sys.version_info.minor == 5: - assert SUPPORTS_BREAKPOINT_BUILTIN is False # type: ignore[comparison-overlap] @pytest.mark.skipif( not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin" diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 67e93b76a49..8f31cb60643 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1049,7 +1049,7 @@ def test_number_precision(self, pytester, config_mode): ("1e3", "999"), # The current implementation doesn't understand that numbers inside # strings shouldn't be treated as numbers: - pytest.param("'3.1416'", "'3.14'", marks=pytest.mark.xfail), # type: ignore + pytest.param("'3.1416'", "'3.14'", marks=pytest.mark.xfail), ], ) def test_number_non_matches(self, pytester, expression, output): diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 2a96f29a12c..2a22f405627 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -1,3 +1,4 @@ +import io from typing import List from typing import Union @@ -95,7 +96,7 @@ def mocked_urlopen_fail(self, monkeypatch): def mocked(url, data): calls.append((url, data)) - raise urllib.error.HTTPError(url, 400, "Bad request", None, None) + raise urllib.error.HTTPError(url, 400, "Bad request", {}, io.BytesIO()) monkeypatch.setattr(urllib.request, "urlopen", mocked) return calls From 09e38b1697b62490c9c2a9a1b2f11ed8c355e089 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 19 Oct 2020 00:00:22 +0300 Subject: [PATCH 700/823] runner: combine a sort+reverse to a sort(reverse=True) Suggested by Zac-HD. --- src/_pytest/runner.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 833d288f0de..794690ddb0b 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -77,8 +77,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: dlist.append(rep) if not dlist: return - dlist.sort(key=lambda x: x.duration) # type: ignore[no-any-return] - dlist.reverse() + dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return] if not durations: tr.write_sep("=", "slowest durations") else: From 23aac10391bb4ff3a7612fdea2b1059a2291675d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2020 03:05:42 +0000 Subject: [PATCH 701/823] build(deps): bump pytest-django in /testing/plugins_integration Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 3.10.0 to 4.0.0. - [Release notes](https://github.com/pytest-dev/pytest-django/releases) - [Changelog](https://github.com/pytest-dev/pytest-django/blob/master/docs/changelog.rst) - [Commits](https://github.com/pytest-dev/pytest-django/compare/v3.10.0...v4.0.0) 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 fcc8767006c..fb75f116889 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -3,7 +3,7 @@ django==3.1.2 pytest-asyncio==0.14.0 pytest-bdd==4.0.1 pytest-cov==2.10.1 -pytest-django==3.10.0 +pytest-django==4.0.0 pytest-flakes==4.0.2 pytest-html==2.1.1 pytest-mock==3.3.1 From f335144d1d7a05f153b7d77f081e9f7bb52232b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2020 03:05:42 +0000 Subject: [PATCH 702/823] build(deps): bump pytest-trio in /testing/plugins_integration Bumps [pytest-trio](https://github.com/python-trio/pytest-trio) from 0.6.0 to 0.7.0. - [Release notes](https://github.com/python-trio/pytest-trio/releases) - [Commits](https://github.com/python-trio/pytest-trio/compare/v0.6.0...v0.7.0) 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 fcc8767006c..effd0a55e09 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -9,7 +9,7 @@ pytest-html==2.1.1 pytest-mock==3.3.1 pytest-rerunfailures==9.1.1 pytest-sugar==0.9.4 -pytest-trio==0.6.0 +pytest-trio==0.7.0 pytest-twisted==1.13.2 twisted==20.3.0 pytest-xvfb==2.0.0 From a642650e173c99a0adaa3df64e9872649932e8ca Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 19 Oct 2020 10:02:36 +0300 Subject: [PATCH 703/823] Drop support for EOL Python 3.5 --- README.rst | 2 +- doc/en/getting-started.rst | 2 +- doc/en/index.rst | 2 +- pyproject.toml | 2 +- src/_pytest/assertion/rewrite.py | 4 +-- src/_pytest/capture.py | 4 +-- src/_pytest/python_api.py | 2 +- testing/python/raises.py | 5 ---- testing/test_reports.py | 45 +++++++------------------------- 9 files changed, 17 insertions(+), 51 deletions(-) diff --git a/README.rst b/README.rst index 057278a926b..398d6451c58 100644 --- a/README.rst +++ b/README.rst @@ -89,7 +89,7 @@ Features - Can run `unittest `_ (or trial), `nose `_ test suites out of the box -- Python 3.5+ and PyPy3 +- Python 3.6+ and PyPy3 - Rich plugin architecture, with over 850+ `external plugins `_ and thriving community diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 5a5f0fa7a43..724827d2971 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -1,7 +1,7 @@ Installation and Getting Started =================================== -**Pythons**: Python 3.5, 3.6, 3.7, 3.8, 3.9, PyPy3 +**Pythons**: Python 3.6, 3.7, 3.8, 3.9, PyPy3 **Platforms**: Linux and Windows diff --git a/doc/en/index.rst b/doc/en/index.rst index a57e9bbacee..ad2057ff14a 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -69,7 +69,7 @@ Features - Can run :ref:`unittest ` (including trial) and :ref:`nose ` test suites out of the box -- Python 3.5+ and PyPy 3 +- Python 3.6+ and PyPy 3 - Rich plugin architecture, with over 315+ `external plugins `_ and thriving community diff --git a/pyproject.toml b/pyproject.toml index 443b94c26a5..dce93a6065d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ filterwarnings = [ "ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest))", # produced by pytest-xdist "ignore:.*type argument to addoption.*:DeprecationWarning", - # produced by python >=3.5 on execnet (pytest-xdist) + # produced on execnet (pytest-xdist) "ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning", # pytest's own futurewarnings "ignore::pytest.PytestExperimentalApiWarning", diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 12c94e99915..649726727c5 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -99,7 +99,7 @@ def find_spec( spec is None # this is a namespace package (without `__init__.py`) # there's nothing to rewrite there - # python3.5 - python3.6: `namespace` + # python3.6: `namespace` # python3.7+: `None` or spec.origin == "namespace" or spec.origin is None @@ -1005,7 +1005,7 @@ def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]: return res, outer_expl def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]: - # From Python 3.5, a Starred node can appear in a function call. + # A Starred node can appear in a function call. res, expl = self.visit(starred.value) new_starred = ast.Starred(res, starred.ctx) return new_starred, "*" + expl diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 4d314998ce2..25535f67b75 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -497,9 +497,7 @@ def writeorg(self, data): class CaptureResult(Generic[AnyStr]): """The result of :method:`CaptureFixture.readouterr`.""" - # Can't use slots in Python<3.5.3 due to https://bugs.python.org/issue31272 - if sys.version_info >= (3, 5, 3): - __slots__ = ("out", "err") + __slots__ = ("out", "err") def __init__(self, out: AnyStr, err: AnyStr) -> None: self.out: AnyStr = out diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 373435236ce..9f4df8e7e9e 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -443,7 +443,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor ``b`` is a "reference value"). You have to specify an absolute tolerance if you want to compare to ``0.0`` because there is no tolerance by - default. Only available in python>=3.5. `More information...`__ + default. `More information...`__ __ https://docs.python.org/3/library/math.html#math.isclose diff --git a/testing/python/raises.py b/testing/python/raises.py index c3580afad45..80634eebfbf 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -162,11 +162,6 @@ def test_raises_cyclic_reference(self, method): class T: def __call__(self): - # Early versions of Python 3.5 have some bug causing the - # __call__ frame to still refer to t even after everything - # is done. This makes the test pass for them. - if sys.version_info < (3, 5, 2): - del self raise ValueError t = T() diff --git a/testing/test_reports.py b/testing/test_reports.py index d18e680b775..b97b1fc2970 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -1,4 +1,3 @@ -import sys from pathlib import Path from typing import Sequence from typing import Union @@ -346,44 +345,18 @@ def test_chained_exceptions_no_reprcrash(self, testdir: Testdir, tw_mock) -> Non from subprocess to main process creates an artificial exception, which ExceptionInfo can't obtain the ReprFileLocation from. """ - # somehow in Python 3.5 on Windows this test fails with: - # File "c:\...\3.5.4\x64\Lib\multiprocessing\connection.py", line 302, in _recv_bytes - # overlapped=True) - # OSError: [WinError 6] The handle is invalid - # - # so in this platform we opted to use a mock traceback which is identical to the - # one produced by the multiprocessing module - if sys.version_info[:2] <= (3, 5) and sys.platform.startswith("win"): - testdir.makepyfile( - """ - # equivalent of multiprocessing.pool.RemoteTraceback - class RemoteTraceback(Exception): - def __init__(self, tb): - self.tb = tb - def __str__(self): - return self.tb - def test_a(): - try: - raise ValueError('value error') - except ValueError as e: - # equivalent to how multiprocessing.pool.rebuild_exc does it - e.__cause__ = RemoteTraceback('runtime error') - raise e + testdir.makepyfile( """ - ) - else: - testdir.makepyfile( - """ - from concurrent.futures import ProcessPoolExecutor + from concurrent.futures import ProcessPoolExecutor - def func(): - raise ValueError('value error') + def func(): + raise ValueError('value error') - def test_a(): - with ProcessPoolExecutor() as p: - p.submit(func).result() - """ - ) + def test_a(): + with ProcessPoolExecutor() as p: + p.submit(func).result() + """ + ) testdir.syspathinsert() reprec = testdir.inline_run() From c9e5042d6d29944b3ed82a08475f3d40f40aa842 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 19 Oct 2020 10:47:35 +0300 Subject: [PATCH 704/823] Remove redundant Python 2.7 code --- testing/code/test_excinfo.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 5f0ade4105c..a55da643068 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,3 +1,4 @@ +import importlib import io import operator import os @@ -20,13 +21,6 @@ from _pytest._io import TerminalWriter from _pytest.pytester import LineMatcher -try: - import importlib -except ImportError: - invalidate_import_caches = None -else: - invalidate_import_caches = getattr(importlib, "invalidate_caches", None) - if TYPE_CHECKING: from _pytest._code.code import _TracebackStyle @@ -445,8 +439,7 @@ def importasmod(source): modpath = tmpdir.join("mod.py") tmpdir.ensure("__init__.py") modpath.write(source) - if invalidate_import_caches is not None: - invalidate_import_caches() + importlib.invalidate_caches() return modpath.pyimport() return importasmod From afaabdda8cdf2b1e333a4aad550e5750d2ecd510 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 19 Oct 2020 18:34:03 +0300 Subject: [PATCH 705/823] cacheprovider: fix some files in packages getting lost from --lf --lf has an optimization where it skips collecting Modules (python files) which don't contain failing tests. The optimization works by getting the paths of all cached failed tests and skipping the collection of Modules whose path is not included in that list. In pytest, Package nodes are Module nodes with the fspath being the file `/__init__.py`. Since it's a Module the logic above triggered for it, and because it's an `__init__.py` file which is unlikely to have any failing tests in it, it is skipped, which causes its entire directory to be skipped, including any Modules inside it with failing tests. Fix by special-casing Packages to never filter. This means entire Packages are never filtered, the Modules themselves are always checked. It is reasonable to consider an optimization which does filter entire packages bases on parent paths etc. but this wouldn't actually save any real work so is really not worth it. --- changelog/7758.bugfix.rst | 1 + src/_pytest/cacheprovider.py | 6 +++++- testing/test_cacheprovider.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 changelog/7758.bugfix.rst diff --git a/changelog/7758.bugfix.rst b/changelog/7758.bugfix.rst new file mode 100644 index 00000000000..a3119b46c0d --- /dev/null +++ b/changelog/7758.bugfix.rst @@ -0,0 +1 @@ +Fixed an issue where some files in packages are getting lost from ``--lf`` even though they contain tests that failed. Regressed in pytest 5.4.0. diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 20f0c71d339..09f3d6653fe 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -28,6 +28,7 @@ from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.python import Module +from _pytest.python import Package from _pytest.reports import TestReport @@ -232,7 +233,10 @@ def __init__(self, lfplugin: "LFPlugin") -> None: def pytest_make_collect_report( self, collector: nodes.Collector ) -> Optional[CollectReport]: - if isinstance(collector, Module): + # Packages are Modules, but _last_failed_paths only contains + # test-bearing paths and doesn't try to include the paths of their + # packages, so don't filter them. + if isinstance(collector, Module) and not isinstance(collector, Package): if Path(str(collector.fspath)) not in self.lfplugin._last_failed_paths: self.lfplugin._skipped_files += 1 diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 6203ebec73b..54e657b27f5 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -7,6 +7,7 @@ import pytest from _pytest.config import ExitCode +from _pytest.pytester import Pytester from _pytest.pytester import Testdir pytest_plugins = ("pytester",) @@ -982,6 +983,36 @@ def test_pass(): pass ) assert result.ret == 0 + def test_packages(self, pytester: Pytester) -> None: + """Regression test for #7758. + + The particular issue here was that Package nodes were included in the + filtering, being themselves Modules for the __init__.py, even if they + had failed Modules in them. + + The tests includes a test in an __init__.py file just to make sure the + fix doesn't somehow regress that, it is not critical for the issue. + """ + pytester.makepyfile( + **{ + "__init__.py": "", + "a/__init__.py": "def test_a_init(): assert False", + "a/test_one.py": "def test_1(): assert False", + "b/__init__.py": "", + "b/test_two.py": "def test_2(): assert False", + }, + ) + pytester.makeini( + """ + [pytest] + python_files = *.py + """ + ) + result = pytester.runpytest() + result.assert_outcomes(failed=3) + result = pytester.runpytest("--lf") + result.assert_outcomes(failed=3) + class TestNewFirst: def test_newfirst_usecase(self, testdir): From fe69d0d680c0f1e688835ed3e688ce088393ccc0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 21 Oct 2020 10:16:23 +0300 Subject: [PATCH 706/823] ci: decrease job timeout from 6 hours to 30 minutes We don't have any jobs that should go beyond that, so let's be nicer to the CI host and quicker to report the failure. --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4125557a8de..5e9367a5d69 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,7 @@ on: jobs: build: runs-on: ${{ matrix.os }} + timeout-minutes: 30 strategy: fail-fast: false @@ -176,6 +177,7 @@ jobs: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest' runs-on: ubuntu-latest + timeout-minutes: 30 needs: [build] From e8504e04f314528bc02eba13d76048295812fac2 Mon Sep 17 00:00:00 2001 From: Matthias Gabriel Date: Thu, 22 Oct 2020 12:11:49 +0200 Subject: [PATCH 707/823] Fix small typo in reference.rst (#7922) Co-authored-by: Ronny Pfannschmidt --- doc/en/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 5f0a20dc746..b243a52bd59 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -189,7 +189,7 @@ Mark a test function as using the given fixture names. When using `usefixtures` in hooks, it can only load fixtures when applied to a test function before test setup (for example in the `pytest_collection_modifyitems` hook). - Also not that his mark has no effect when applied to **fixtures**. + Also note that this mark has no effect when applied to **fixtures**. From 03363473f75a2c796c9f78e3de0a46a528dc1299 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 23 Oct 2020 14:03:21 +0100 Subject: [PATCH 708/823] expand feature request issue template --- .github/ISSUE_TEMPLATE/2_feature_request.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/2_feature_request.md b/.github/ISSUE_TEMPLATE/2_feature_request.md index 54912b05233..7fe50371b5c 100644 --- a/.github/ISSUE_TEMPLATE/2_feature_request.md +++ b/.github/ISSUE_TEMPLATE/2_feature_request.md @@ -3,3 +3,23 @@ name: 🚀 Feature Request about: Ideas for new features and improvements --- + + + +#### What's the problem this feature will solve? + + +#### Describe the solution you'd like + + + + +#### Alternative Solutions + + +#### Additional context + From 0d9e27a363c91f28e58e5825ff3b8f7ad8a5daee Mon Sep 17 00:00:00 2001 From: Emiel van de Laar Date: Fri, 23 Oct 2020 16:31:17 +0200 Subject: [PATCH 709/823] doc: Remove unused imports in examples (#7924) The "os" imports in the `tmp_path` and `tmpdir` fixture examples are unused and thus have been removed to prevent confusion. --- doc/en/tmpdir.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index a0d5cc0de0a..c8da5877b28 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -20,8 +20,6 @@ created in the `base temporary directory`_. .. code-block:: python # content of test_tmp_path.py - import os - CONTENT = "content" @@ -97,9 +95,6 @@ and more. Here is an example test usage: .. code-block:: python # content of test_tmpdir.py - import os - - def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") p.write("content") From 1c0c56dfb99bd404071716612bb0ca5928b02e98 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 23 Oct 2020 21:01:31 +0300 Subject: [PATCH 710/823] pytester: minor doc fixes --- src/_pytest/pytester.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index dc234dc6356..a1768363bb3 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -771,9 +771,9 @@ def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: .. code-block:: python - testdir.makefile(".txt", "line1", "line2") + pytester.makefile(".txt", "line1", "line2") - testdir.makefile(".ini", pytest="[pytest]\naddopts=-rs\n") + pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n") """ return self._makefile(ext, args, kwargs) @@ -808,11 +808,11 @@ def makepyfile(self, *args, **kwargs) -> Path: .. code-block:: python - def test_something(testdir): + def test_something(pytester): # Initial file is created test_something.py. - testdir.makepyfile("foobar") + pytester.makepyfile("foobar") # To create multiple files, pass kwargs accordingly. - testdir.makepyfile(custom="foobar") + pytester.makepyfile(custom="foobar") # At this point, both 'test_something.py' & 'custom.py' exist in the test directory. """ @@ -828,11 +828,11 @@ def maketxtfile(self, *args, **kwargs) -> Path: .. code-block:: python - def test_something(testdir): + def test_something(pytester): # Initial file is created test_something.txt. - testdir.maketxtfile("foobar") + pytester.maketxtfile("foobar") # To create multiple files, pass kwargs accordingly. - testdir.maketxtfile(custom="foobar") + pytester.maketxtfile(custom="foobar") # At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory. """ @@ -1279,7 +1279,7 @@ def popen( ) kw["env"] = env - if stdin is Testdir.CLOSE_STDIN: + if stdin is self.CLOSE_STDIN: kw["stdin"] = subprocess.PIPE elif isinstance(stdin, bytes): kw["stdin"] = subprocess.PIPE @@ -1287,7 +1287,7 @@ def popen( kw["stdin"] = stdin popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) - if stdin is Testdir.CLOSE_STDIN: + if stdin is self.CLOSE_STDIN: assert popen.stdin is not None popen.stdin.close() elif isinstance(stdin, bytes): @@ -1311,7 +1311,7 @@ def run( being converted to ``str`` automatically. :param timeout: The period in seconds after which to timeout and raise - :py:class:`Testdir.TimeoutExpired`. + :py:class:`Pytester.TimeoutExpired`. :param stdin: Optional standard input. Bytes are being send, closing the pipe, otherwise it is passed through to ``popen``. @@ -1412,7 +1412,7 @@ def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunRes The sequence of arguments to pass to the pytest subprocess. :param timeout: The period in seconds after which to timeout and raise - :py:class:`Testdir.TimeoutExpired`. + :py:class:`Pytester.TimeoutExpired`. :rtype: RunResult """ @@ -1453,9 +1453,8 @@ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": pytest.skip("pexpect.spawn not available") logfile = self.path.joinpath("spawn.out").open("wb") - child = pexpect.spawn(cmd, logfile=logfile) + child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout) self._request.addfinalizer(logfile.close) - child.timeout = expect_timeout return child From 50114d4731876daee8b6f3b3349e6ee774ac8287 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 23 Oct 2020 18:51:51 +0300 Subject: [PATCH 711/823] python: fix quadratic behavior in collection of items using xunit fixtures Since commit 0f918b1a9dd14a2d046 pytest uses auto-generated autouse pytest fixtures for the xunit fixtures {setup,teardown}_{module,class,method,function}. All of these fixtures were given the same name. Unfortunately, pytest fixture lookup for a name works by grabbing all of the fixtures globally declared with a name and filtering to only those which match the specific node. So each xunit-using item iterates over a list (of fixturedefs) of a size of all previous same-xunit-using items, i.e. quadratic. Fixing this properly to use a better data structure is likely to take some effort, but we can avoid the immediate problem by just using a different name for each item's autouse fixture, so it only matches itself. A benchmark is added to demonstrate the issue. It is still way too slow after the fix and possibly still quadratic, but for a different reason which is another matter. Running --collect-only, before (snipped): 202533232 function calls (201902604 primitive calls) in 86.379 seconds ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 85.688 85.688 main.py:320(pytest_collection) 1 0.000 0.000 85.688 85.688 main.py:567(perform_collect) 80557/556 0.021 0.000 85.050 0.153 {method 'extend' of 'list' objects} 85001/15001 0.166 0.000 85.045 0.006 main.py:785(genitems) 10002 0.050 0.000 84.219 0.008 runner.py:455(collect_one_node) 10002 0.049 0.000 83.763 0.008 runner.py:340(pytest_make_collect_report) 10002 0.079 0.000 83.668 0.008 runner.py:298(from_call) 10002 0.019 0.000 83.541 0.008 runner.py:341() 5001 0.184 0.000 81.922 0.016 python.py:412(collect) 5000 0.020 0.000 81.072 0.016 python.py:842(collect) 30003 0.118 0.000 78.478 0.003 python.py:218(pytest_pycollect_makeitem) 30000 0.190 0.000 77.957 0.003 python.py:450(_genfunctions) 40001 0.081 0.000 76.664 0.002 nodes.py:183(from_parent) 30000 0.087 0.000 76.629 0.003 python.py:1595(from_parent) 40002 0.092 0.000 76.583 0.002 nodes.py:102(_create) 30000 0.305 0.000 76.404 0.003 python.py:1533(__init__) 15000 0.132 0.000 74.765 0.005 fixtures.py:1439(getfixtureinfo) 15000 0.165 0.000 73.664 0.005 fixtures.py:1492(getfixtureclosure) 15000 0.044 0.000 57.584 0.004 fixtures.py:1653(getfixturedefs) 30000 18.840 0.001 57.540 0.002 fixtures.py:1668(_matchfactories) 37507500 31.352 0.000 38.700 0.000 nodes.py:76(ischildnode) 15000 10.464 0.001 15.806 0.001 fixtures.py:1479(_getautousenames) 112930587/112910019 7.333 0.000 7.339 0.000 {built-in method builtins.len} After: 51890333 function calls (51259706 primitive calls) in 27.306 seconds ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 26.783 26.783 main.py:320(pytest_collection) 1 0.000 0.000 26.783 26.783 main.py:567(perform_collect) 80557/556 0.020 0.000 26.108 0.047 {method 'extend' of 'list' objects} 85001/15001 0.151 0.000 26.103 0.002 main.py:785(genitems) 10002 0.047 0.000 25.324 0.003 runner.py:455(collect_one_node) 10002 0.045 0.000 24.888 0.002 runner.py:340(pytest_make_collect_report) 10002 0.069 0.000 24.805 0.002 runner.py:298(from_call) 10002 0.017 0.000 24.690 0.002 runner.py:341() 5001 0.168 0.000 23.150 0.005 python.py:412(collect) 5000 0.019 0.000 22.223 0.004 python.py:858(collect) 30003 0.101 0.000 19.818 0.001 python.py:218(pytest_pycollect_makeitem) 30000 0.161 0.000 19.368 0.001 python.py:450(_genfunctions) 30000 0.302 0.000 18.236 0.001 python.py:1611(from_parent) 40001 0.084 0.000 18.051 0.000 nodes.py:183(from_parent) 40002 0.116 0.000 17.967 0.000 nodes.py:102(_create) 30000 0.308 0.000 17.770 0.001 python.py:1549(__init__) 15000 0.117 0.000 16.111 0.001 fixtures.py:1439(getfixtureinfo) 15000 0.134 0.000 15.135 0.001 fixtures.py:1492(getfixtureclosure) 15000 9.320 0.001 14.738 0.001 fixtures.py:1479(_getautousenames) --- bench/xunit.py | 11 +++++++++++ src/_pytest/python.py | 28 ++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 bench/xunit.py diff --git a/bench/xunit.py b/bench/xunit.py new file mode 100644 index 00000000000..3a77dcdce42 --- /dev/null +++ b/bench/xunit.py @@ -0,0 +1,11 @@ +for i in range(5000): + exec( + f""" +class Test{i}: + @classmethod + def setup_class(cls): pass + def test_1(self): pass + def test_2(self): pass + def test_3(self): pass +""" + ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 07cc4d99cf4..35797cc0762 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -522,7 +522,12 @@ def _inject_setup_module_fixture(self) -> None: if setup_module is None and teardown_module is None: return - @fixtures.fixture(autouse=True, scope="module") + @fixtures.fixture( + autouse=True, + scope="module", + # Use a unique name to speed up lookup. + name=f"xunit_setup_module_fixture_{self.obj.__name__}", + ) def xunit_setup_module_fixture(request) -> Generator[None, None, None]: if setup_module is not None: _call_with_optional_argument(setup_module, request.module) @@ -546,7 +551,12 @@ def _inject_setup_function_fixture(self) -> None: if setup_function is None and teardown_function is None: return - @fixtures.fixture(autouse=True, scope="function") + @fixtures.fixture( + autouse=True, + scope="function", + # Use a unique name to speed up lookup. + name=f"xunit_setup_function_fixture_{self.obj.__name__}", + ) def xunit_setup_function_fixture(request) -> Generator[None, None, None]: if request.instance is not None: # in this case we are bound to an instance, so we need to let @@ -789,7 +799,12 @@ def _inject_setup_class_fixture(self) -> None: if setup_class is None and teardown_class is None: return - @fixtures.fixture(autouse=True, scope="class") + @fixtures.fixture( + autouse=True, + scope="class", + # Use a unique name to speed up lookup. + name=f"xunit_setup_class_fixture_{self.obj.__qualname__}", + ) def xunit_setup_class_fixture(cls) -> Generator[None, None, None]: if setup_class is not None: func = getimfunc(setup_class) @@ -813,7 +828,12 @@ def _inject_setup_method_fixture(self) -> None: if setup_method is None and teardown_method is None: return - @fixtures.fixture(autouse=True, scope="function") + @fixtures.fixture( + autouse=True, + scope="function", + # Use a unique name to speed up lookup. + name=f"xunit_setup_method_fixture_{self.obj.__qualname__}", + ) def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]: method = request.function if setup_method is not None: From e14b724ff4002ccd87d4e82f981128cfde45178a Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 23 Oct 2020 22:35:56 +0100 Subject: [PATCH 712/823] Update .github/ISSUE_TEMPLATE/2_feature_request.md --- .github/ISSUE_TEMPLATE/2_feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/2_feature_request.md b/.github/ISSUE_TEMPLATE/2_feature_request.md index 7fe50371b5c..01fe96295ea 100644 --- a/.github/ISSUE_TEMPLATE/2_feature_request.md +++ b/.github/ISSUE_TEMPLATE/2_feature_request.md @@ -5,7 +5,7 @@ about: Ideas for new features and improvements --- From 751575fa97f2551582414a3975dc7b556a88ca94 Mon Sep 17 00:00:00 2001 From: symonk Date: Sat, 24 Oct 2020 10:59:25 +0100 Subject: [PATCH 713/823] make some hookspec docstrings technically correct --- src/_pytest/hookspec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index aa0b5cef4a6..33ca782cf49 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -446,7 +446,7 @@ def pytest_runtest_logstart( See :func:`pytest_runtest_protocol` for a description of the runtest protocol. :param str nodeid: Full node ID of the item. - :param location: A triple of ``(filename, lineno, testname)``. + :param location: A tuple of ``(filename, lineno, testname)``. """ @@ -458,7 +458,7 @@ def pytest_runtest_logfinish( See :func:`pytest_runtest_protocol` for a description of the runtest protocol. :param str nodeid: Full node ID of the item. - :param location: A triple of ``(filename, lineno, testname)``. + :param location: A tuple of ``(filename, lineno, testname)``. """ From aa0e2d654fb0c8ac18747fe1cdf54d8f29bcd24a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 24 Oct 2020 02:09:28 +0300 Subject: [PATCH 714/823] fixtures: use a faster replacement for ischildnode ischildnode can be quite hot in some cases involving many fixtures. However it is always used in a way that the nodeid is constant and the baseid is iterated. So we can save work by pre-computing the parents of the nodeid and use a simple containment test. The `_getautousenames` function has the same stuff open-coded, so change it to use the new function as well. --- src/_pytest/fixtures.py | 11 +++---- src/_pytest/nodes.py | 68 ++++++++++++++++++----------------------- testing/test_nodes.py | 29 ++++++++++-------- 3 files changed, 51 insertions(+), 57 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 94171b5f667..6bd9e4cd6c5 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1478,14 +1478,10 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: def _getautousenames(self, nodeid: str) -> List[str]: """Return a list of fixture names to be used.""" + parentnodeids = set(nodes.iterparentnodeids(nodeid)) autousenames: List[str] = [] for baseid, basenames in self._nodeid_and_autousenames: - if nodeid.startswith(baseid): - if baseid: - i = len(baseid) - nextchar = nodeid[i : i + 1] - if nextchar and nextchar not in ":/": - continue + if baseid in parentnodeids: autousenames.extend(basenames) return autousenames @@ -1668,6 +1664,7 @@ def getfixturedefs( def _matchfactories( self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str ) -> Iterator[FixtureDef[Any]]: + parentnodeids = set(nodes.iterparentnodeids(nodeid)) for fixturedef in fixturedefs: - if nodes.ischildnode(fixturedef.baseid, nodeid): + if fixturedef.baseid in parentnodeids: yield fixturedef diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 6ab08953a7a..dd58d5df9fd 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,6 +1,5 @@ import os import warnings -from functools import lru_cache from pathlib import Path from typing import Callable from typing import Iterable @@ -44,46 +43,39 @@ tracebackcutdir = py.path.local(_pytest.__file__).dirpath() -@lru_cache(maxsize=None) -def _splitnode(nodeid: str) -> Tuple[str, ...]: - """Split a nodeid into constituent 'parts'. +def iterparentnodeids(nodeid: str) -> Iterator[str]: + """Return the parent node IDs of a given node ID, inclusive. - Node IDs are strings, and can be things like: - '' - 'testing/code' - 'testing/code/test_excinfo.py' - 'testing/code/test_excinfo.py::TestFormattedExcinfo' + For the node ID - Return values are lists e.g. - [] - ['testing', 'code'] - ['testing', 'code', 'test_excinfo.py'] - ['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo'] - """ - if nodeid == "": - # If there is no root node at all, return an empty list so the caller's - # logic can remain sane. - return () - parts = nodeid.split(SEP) - # Replace single last element 'test_foo.py::Bar' with multiple elements - # 'test_foo.py', 'Bar'. - parts[-1:] = parts[-1].split("::") - # Convert parts into a tuple to avoid possible errors with caching of a - # mutable type. - return tuple(parts) - - -def ischildnode(baseid: str, nodeid: str) -> bool: - """Return True if the nodeid is a child node of the baseid. - - E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', - but not of 'foo/blorp'. + "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source" + + the result would be + + "" + "testing" + "testing/code" + "testing/code/test_excinfo.py" + "testing/code/test_excinfo.py::TestFormattedExcinfo" + "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source" + + Note that :: parts are only considered at the last / component. """ - base_parts = _splitnode(baseid) - node_parts = _splitnode(nodeid) - if len(node_parts) < len(base_parts): - return False - return node_parts[: len(base_parts)] == base_parts + pos = 0 + sep = SEP + yield "" + while True: + at = nodeid.find(sep, pos) + if at == -1 and sep == SEP: + sep = "::" + elif at == -1: + if nodeid: + yield nodeid + break + else: + if at: + yield nodeid[:at] + pos = at + len(sep) _NodeType = TypeVar("_NodeType", bound="Node") diff --git a/testing/test_nodes.py b/testing/test_nodes.py index f9026ec619f..b72a94ebeb0 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -1,3 +1,5 @@ +from typing import List + import py import pytest @@ -6,21 +8,24 @@ @pytest.mark.parametrize( - "baseid, nodeid, expected", + ("nodeid", "expected"), ( - ("", "", True), - ("", "foo", True), - ("", "foo/bar", True), - ("", "foo/bar::TestBaz", True), - ("foo", "food", False), - ("foo/bar::TestBaz", "foo/bar", False), - ("foo/bar::TestBaz", "foo/bar::TestBop", False), - ("foo/bar", "foo/bar::TestBop", True), + ("", [""]), + ("a", ["", "a"]), + ("aa/b", ["", "aa", "aa/b"]), + ("a/b/c", ["", "a", "a/b", "a/b/c"]), + ("a/bbb/c::D", ["", "a", "a/bbb", "a/bbb/c", "a/bbb/c::D"]), + ("a/b/c::D::eee", ["", "a", "a/b", "a/b/c", "a/b/c::D", "a/b/c::D::eee"]), + # :: considered only at the last component. + ("::xx", ["", "::xx"]), + ("a/b/c::D/d::e", ["", "a", "a/b", "a/b/c::D", "a/b/c::D/d", "a/b/c::D/d::e"]), + # : alone is not a separator. + ("a/b::D:e:f::g", ["", "a", "a/b", "a/b::D:e:f", "a/b::D:e:f::g"]), ), ) -def test_ischildnode(baseid: str, nodeid: str, expected: bool) -> None: - result = nodes.ischildnode(baseid, nodeid) - assert result is expected +def test_iterparentnodeids(nodeid: str, expected: List[str]) -> None: + result = list(nodes.iterparentnodeids(nodeid)) + assert result == expected def test_node_from_parent_disallowed_arguments() -> None: From d6becfa177a8105696fb6fd16777312a5e9c601e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 24 Oct 2020 02:17:33 +0300 Subject: [PATCH 715/823] fixtures: change _getautousenames to an iterator This reads better. --- src/_pytest/fixtures.py | 10 ++++------ testing/python/fixtures.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 6bd9e4cd6c5..644db3603ef 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1476,14 +1476,12 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: self.parsefactories(plugin, nodeid) - def _getautousenames(self, nodeid: str) -> List[str]: - """Return a list of fixture names to be used.""" + def _getautousenames(self, nodeid: str) -> Iterator[str]: + """Return the names of autouse fixtures applicable to nodeid.""" parentnodeids = set(nodes.iterparentnodeids(nodeid)) - autousenames: List[str] = [] for baseid, basenames in self._nodeid_and_autousenames: if baseid in parentnodeids: - autousenames.extend(basenames) - return autousenames + yield from basenames def getfixtureclosure( self, @@ -1499,7 +1497,7 @@ def getfixtureclosure( # (discovering matching fixtures for a given name/node is expensive). parentid = parentnode.nodeid - fixturenames_closure = self._getautousenames(parentid) + fixturenames_closure = list(self._getautousenames(parentid)) def merge(otherlist: Iterable[str]) -> None: for arg in otherlist: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index bd82125e7d0..a4838ee5167 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1710,7 +1710,7 @@ def test_parsefactories_conftest(self, testdir): """ from _pytest.pytester import get_public_names def test_check_setup(item, fm): - autousenames = fm._getautousenames(item.nodeid) + autousenames = list(fm._getautousenames(item.nodeid)) assert len(get_public_names(autousenames)) == 2 assert "perfunction2" in autousenames assert "perfunction" in autousenames From 470ea504e2227f879103782b76810447b1923214 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 24 Oct 2020 00:22:13 +0300 Subject: [PATCH 716/823] fixtures: fix quadratic behavior in the number of autouse fixtures It turns out all autouse fixtures are kept in a global list, and thinned out for a particular node using a linear scan of the entire list each time. Change the list to a dict, and only take the nodes we need. --- changelog/4824.bugfix.rst | 1 + src/_pytest/fixtures.py | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 changelog/4824.bugfix.rst diff --git a/changelog/4824.bugfix.rst b/changelog/4824.bugfix.rst new file mode 100644 index 00000000000..f2e6db7ab0f --- /dev/null +++ b/changelog/4824.bugfix.rst @@ -0,0 +1 @@ +Fixed quadratic behavior and improved performance of collection of items using autouse fixtures and xunit fixtures. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 644db3603ef..18094f21c3b 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1412,9 +1412,10 @@ def __init__(self, session: "Session") -> None: self.config: Config = session.config self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {} self._holderobjseen: Set[object] = set() - self._nodeid_and_autousenames: List[Tuple[str, List[str]]] = [ - ("", self.config.getini("usefixtures")) - ] + # A mapping from a nodeid to a list of autouse fixtures it defines. + self._nodeid_autousenames: Dict[str, List[str]] = { + "": self.config.getini("usefixtures"), + } session.config.pluginmanager.register(self, "funcmanage") def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]: @@ -1478,9 +1479,9 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: def _getautousenames(self, nodeid: str) -> Iterator[str]: """Return the names of autouse fixtures applicable to nodeid.""" - parentnodeids = set(nodes.iterparentnodeids(nodeid)) - for baseid, basenames in self._nodeid_and_autousenames: - if baseid in parentnodeids: + for parentnodeid in nodes.iterparentnodeids(nodeid): + basenames = self._nodeid_autousenames.get(parentnodeid) + if basenames: yield from basenames def getfixtureclosure( @@ -1642,7 +1643,7 @@ def parsefactories( autousenames.append(name) if autousenames: - self._nodeid_and_autousenames.append((nodeid or "", autousenames)) + self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames) def getfixturedefs( self, argname: str, nodeid: str From d9ac2efbcdee123f73edf1829c8bd6a91be3b6d2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 25 Oct 2020 01:08:12 +0200 Subject: [PATCH 717/823] testing: python 3.10 fix --- testing/test_runner.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/testing/test_runner.py b/testing/test_runner.py index 95b8f5fccba..a1f1db48d06 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -336,10 +336,9 @@ def test_method(self): assert reps[2].failed assert reps[2].when == "teardown" assert reps[2].longrepr.reprcrash.message in ( - # python3 error "TypeError: teardown_method() missing 2 required positional arguments: 'y' and 'z'", - # python2 error - "TypeError: teardown_method() takes exactly 4 arguments (2 given)", + # Python >= 3.10 + "TypeError: TestClass.teardown_method() missing 2 required positional arguments: 'y' and 'z'", ) def test_failure_in_setup_function_ignores_custom_repr(self, testdir) -> None: From 25dee8fef62518ac59ca5a06faf10343cefd2b95 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 25 Oct 2020 01:21:47 +0200 Subject: [PATCH 718/823] testing: fix test_assertrewrite with PYTHONPYCACHEPREFIX Make the tests work when running with PYTHONPYCACHEPREFIX (possible when running in a dirty environment, not under tox). --- testing/test_assertrewrite.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index f95fd54b3eb..251e35684c8 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -786,6 +786,8 @@ def test_rewritten(): sub.chmod(old_mode) def test_dont_write_bytecode(self, testdir, monkeypatch): + monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) + testdir.makepyfile( """ import os @@ -797,7 +799,10 @@ def test_no_bytecode(): monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") assert testdir.runpytest_subprocess().ret == 0 - def test_orphaned_pyc_file(self, testdir): + def test_orphaned_pyc_file(self, testdir, monkeypatch): + monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) + monkeypatch.setattr(sys, "pycache_prefix", None, raising=False) + testdir.makepyfile( """ import orphan @@ -826,6 +831,7 @@ def test_it(): def test_cached_pyc_includes_pytest_version(self, testdir, monkeypatch): """Avoid stale caches (#1671)""" monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) + monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) testdir.makepyfile( test_foo=""" def test_foo(): @@ -852,11 +858,13 @@ def test_optimized(): tmp = "--basetemp=%s" % p monkeypatch.setenv("PYTHONOPTIMIZE", "2") monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) + monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) assert testdir.runpytest_subprocess(tmp).ret == 0 tagged = "test_pyc_vs_pyo." + PYTEST_TAG assert tagged + ".pyo" in os.listdir("__pycache__") monkeypatch.undo() monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) + monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) assert testdir.runpytest_subprocess(tmp).ret == 1 assert tagged + ".pyc" in os.listdir("__pycache__") @@ -1592,10 +1600,11 @@ class TestPyCacheDir: ], ) def test_get_cache_dir(self, monkeypatch, prefix, source, expected): - if prefix: - if sys.version_info < (3, 8): - pytest.skip("pycache_prefix not available in py<38") - monkeypatch.setattr(sys, "pycache_prefix", prefix) + 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) From 897f151e94ad4a16721510fbdbf935f8344aad0a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 25 Oct 2020 02:03:20 +0200 Subject: [PATCH 719/823] testing: use pytester.spawn instead of testdir Part of investigating a bug, but didn't fix it. --- testing/test_debugging.py | 354 +++++++++++++++++++++----------------- testing/test_pytester.py | 11 +- testing/test_terminal.py | 14 +- 3 files changed, 205 insertions(+), 174 deletions(-) diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 2760930efb1..ed96f7ec781 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -1,9 +1,12 @@ import os import sys +from typing import List import _pytest._code import pytest from _pytest.debugging import _validate_usepdb_cls +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Pytester try: # Type ignored for Python <= 3.6. @@ -19,22 +22,22 @@ @pytest.fixture(autouse=True) def pdb_env(request): - if "testdir" in request.fixturenames: + if "pytester" in request.fixturenames: # Disable pdb++ with inner tests. - testdir = request.getfixturevalue("testdir") - testdir.monkeypatch.setenv("PDBPP_HIJACK_PDB", "0") + pytester = request.getfixturevalue("testdir") + pytester.monkeypatch.setenv("PDBPP_HIJACK_PDB", "0") -def runpdb_and_get_report(testdir, source): - p = testdir.makepyfile(source) - result = testdir.runpytest_inprocess("--pdb", p) - reports = result.reprec.getreports("pytest_runtest_logreport") +def runpdb_and_get_report(pytester: Pytester, source: str): + p = pytester.makepyfile(source) + result = pytester.runpytest_inprocess("--pdb", p) + reports = result.reprec.getreports("pytest_runtest_logreport") # type: ignore[attr-defined] assert len(reports) == 3, reports # setup/call/teardown return reports[1] @pytest.fixture -def custom_pdb_calls(): +def custom_pdb_calls() -> List[str]: called = [] # install dummy debugger class and track which methods were called on it @@ -91,9 +94,9 @@ def mypdb(*args): monkeypatch.setattr(plugin, "post_mortem", mypdb) return pdblist - def test_pdb_on_fail(self, testdir, pdblist): + def test_pdb_on_fail(self, pytester: Pytester, pdblist) -> None: rep = runpdb_and_get_report( - testdir, + pytester, """ def test_func(): assert 0 @@ -104,9 +107,9 @@ def test_func(): tb = _pytest._code.Traceback(pdblist[0][0]) assert tb[-1].name == "test_func" - def test_pdb_on_xfail(self, testdir, pdblist): + def test_pdb_on_xfail(self, pytester: Pytester, pdblist) -> None: rep = runpdb_and_get_report( - testdir, + pytester, """ import pytest @pytest.mark.xfail @@ -117,9 +120,9 @@ def test_func(): assert "xfail" in rep.keywords assert not pdblist - def test_pdb_on_skip(self, testdir, pdblist): + def test_pdb_on_skip(self, pytester, pdblist) -> None: rep = runpdb_and_get_report( - testdir, + pytester, """ import pytest def test_func(): @@ -129,9 +132,9 @@ def test_func(): assert rep.skipped assert len(pdblist) == 0 - def test_pdb_on_BdbQuit(self, testdir, pdblist): + def test_pdb_on_BdbQuit(self, pytester, pdblist) -> None: rep = runpdb_and_get_report( - testdir, + pytester, """ import bdb def test_func(): @@ -141,9 +144,9 @@ def test_func(): assert rep.failed assert len(pdblist) == 0 - def test_pdb_on_KeyboardInterrupt(self, testdir, pdblist): + def test_pdb_on_KeyboardInterrupt(self, pytester, pdblist) -> None: rep = runpdb_and_get_report( - testdir, + pytester, """ def test_func(): raise KeyboardInterrupt @@ -160,8 +163,8 @@ def flush(child): child.wait() assert not child.isalive() - def test_pdb_unittest_postmortem(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_unittest_postmortem(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import unittest class Blub(unittest.TestCase): @@ -172,7 +175,7 @@ def test_false(self): assert 0 """ ) - child = testdir.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest(f"--pdb {p1}") child.expect("Pdb") child.sendline("p self.filename") child.sendeof() @@ -180,9 +183,9 @@ def test_false(self): assert "debug.me" in rest self.flush(child) - def test_pdb_unittest_skip(self, testdir): + def test_pdb_unittest_skip(self, pytester: Pytester) -> None: """Test for issue #2137""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import unittest @unittest.skipIf(True, 'Skipping also with pdb active') @@ -191,14 +194,14 @@ def test_one(self): assert 0 """ ) - child = testdir.spawn_pytest("-rs --pdb %s" % p1) + child = pytester.spawn_pytest(f"-rs --pdb {p1}") child.expect("Skipping also with pdb active") child.expect_exact("= 1 skipped in") child.sendeof() self.flush(child) - def test_pdb_print_captured_stdout_and_stderr(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_print_captured_stdout_and_stderr(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def test_1(): import sys @@ -210,7 +213,7 @@ def test_not_called_due_to_quit(): pass """ ) - child = testdir.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest("--pdb %s" % p1) child.expect("captured stdout") child.expect("get rekt") child.expect("captured stderr") @@ -226,14 +229,16 @@ def test_not_called_due_to_quit(): assert "get rekt" not in rest self.flush(child) - def test_pdb_dont_print_empty_captured_stdout_and_stderr(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_dont_print_empty_captured_stdout_and_stderr( + self, pytester: Pytester + ) -> None: + p1 = pytester.makepyfile( """ def test_1(): assert False """ ) - child = testdir.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest("--pdb %s" % p1) child.expect("Pdb") output = child.before.decode("utf8") child.sendeof() @@ -242,8 +247,8 @@ def test_1(): self.flush(child) @pytest.mark.parametrize("showcapture", ["all", "no", "log"]) - def test_pdb_print_captured_logs(self, testdir, showcapture): - p1 = testdir.makepyfile( + def test_pdb_print_captured_logs(self, pytester, showcapture: str) -> None: + p1 = pytester.makepyfile( """ def test_1(): import logging @@ -251,7 +256,7 @@ def test_1(): assert False """ ) - child = testdir.spawn_pytest(f"--show-capture={showcapture} --pdb {p1}") + child = pytester.spawn_pytest(f"--show-capture={showcapture} --pdb {p1}") if showcapture in ("all", "log"): child.expect("captured log") child.expect("get rekt") @@ -261,8 +266,8 @@ def test_1(): assert "1 failed" in rest self.flush(child) - def test_pdb_print_captured_logs_nologging(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_print_captured_logs_nologging(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def test_1(): import logging @@ -270,7 +275,7 @@ def test_1(): assert False """ ) - child = testdir.spawn_pytest("--show-capture=all --pdb -p no:logging %s" % p1) + child = pytester.spawn_pytest("--show-capture=all --pdb -p no:logging %s" % p1) child.expect("get rekt") output = child.before.decode("utf8") assert "captured log" not in output @@ -280,8 +285,8 @@ def test_1(): assert "1 failed" in rest self.flush(child) - def test_pdb_interaction_exception(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_interaction_exception(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest def globalfunc(): @@ -290,7 +295,7 @@ def test_1(): pytest.raises(ValueError, globalfunc) """ ) - child = testdir.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest("--pdb %s" % p1) child.expect(".*def test_1") child.expect(".*pytest.raises.*globalfunc") child.expect("Pdb") @@ -300,29 +305,29 @@ def test_1(): child.expect("1 failed") self.flush(child) - def test_pdb_interaction_on_collection_issue181(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_interaction_on_collection_issue181(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest xxx """ ) - child = testdir.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest("--pdb %s" % p1) # child.expect(".*import pytest.*") child.expect("Pdb") child.sendline("c") child.expect("1 error") self.flush(child) - def test_pdb_interaction_on_internal_error(self, testdir): - testdir.makeconftest( + def test_pdb_interaction_on_internal_error(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_runtest_protocol(): 0/0 """ ) - p1 = testdir.makepyfile("def test_func(): pass") - child = testdir.spawn_pytest("--pdb %s" % p1) + p1 = pytester.makepyfile("def test_func(): pass") + child = pytester.spawn_pytest("--pdb %s" % p1) child.expect("Pdb") # INTERNALERROR is only displayed once via terminal reporter. @@ -340,17 +345,24 @@ def pytest_runtest_protocol(): child.sendeof() self.flush(child) - def test_pdb_prevent_ConftestImportFailure_hiding_exception(self, testdir): - testdir.makepyfile("def test_func(): pass") - sub_dir = testdir.tmpdir.join("ns").ensure_dir() - sub_dir.join("conftest").new(ext=".py").write("import unknown") - sub_dir.join("test_file").new(ext=".py").write("def test_func(): pass") + def test_pdb_prevent_ConftestImportFailure_hiding_exception( + self, pytester: Pytester + ) -> None: + pytester.makepyfile("def test_func(): pass") + sub_dir = pytester.path.joinpath("ns") + sub_dir.mkdir() + sub_dir.joinpath("conftest").with_suffix(".py").write_text( + "import unknown", "utf-8" + ) + sub_dir.joinpath("test_file").with_suffix(".py").write_text( + "def test_func(): pass", "utf-8" + ) - result = testdir.runpytest_subprocess("--pdb", ".") + result = pytester.runpytest_subprocess("--pdb", ".") result.stdout.fnmatch_lines(["-> import unknown"]) - def test_pdb_interaction_capturing_simple(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_interaction_capturing_simple(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest def test_1(): @@ -361,7 +373,7 @@ def test_1(): assert 0 """ ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect(r"test_1\(\)") child.expect("i == 1") child.expect("Pdb") @@ -373,8 +385,8 @@ def test_1(): assert "hello17" in rest # out is captured self.flush(child) - def test_pdb_set_trace_kwargs(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_set_trace_kwargs(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest def test_1(): @@ -385,7 +397,7 @@ def test_1(): assert 0 """ ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect("== my_header ==") assert "PDB set_trace" not in child.before.decode() child.expect("Pdb") @@ -396,15 +408,15 @@ def test_1(): assert "hello17" in rest # out is captured self.flush(child) - def test_pdb_set_trace_interception(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_set_trace_interception(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pdb def test_1(): pdb.set_trace() """ ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect("test_1") child.expect("Pdb") child.sendline("q") @@ -414,8 +426,8 @@ def test_1(): assert "BdbQuit" not in rest self.flush(child) - def test_pdb_and_capsys(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_and_capsys(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest def test_1(capsys): @@ -423,7 +435,7 @@ def test_1(capsys): pytest.set_trace() """ ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect("test_1") child.send("capsys.readouterr()\n") child.expect("hello1") @@ -431,8 +443,8 @@ def test_1(capsys): child.read() self.flush(child) - def test_pdb_with_caplog_on_pdb_invocation(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_with_caplog_on_pdb_invocation(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def test_1(capsys, caplog): import logging @@ -440,7 +452,7 @@ def test_1(capsys, caplog): assert 0 """ ) - child = testdir.spawn_pytest("--pdb %s" % str(p1)) + child = pytester.spawn_pytest("--pdb %s" % str(p1)) child.send("caplog.record_tuples\n") child.expect_exact( "[('test_pdb_with_caplog_on_pdb_invocation', 30, 'some_warning')]" @@ -449,8 +461,8 @@ def test_1(capsys, caplog): child.read() self.flush(child) - def test_set_trace_capturing_afterwards(self, testdir): - p1 = testdir.makepyfile( + def test_set_trace_capturing_afterwards(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pdb def test_1(): @@ -460,7 +472,7 @@ def test_2(): assert 0 """ ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect("test_1") child.send("c\n") child.expect("test_2") @@ -470,8 +482,8 @@ def test_2(): child.read() self.flush(child) - def test_pdb_interaction_doctest(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_interaction_doctest(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def function_1(): ''' @@ -480,7 +492,7 @@ def function_1(): ''' """ ) - child = testdir.spawn_pytest("--doctest-modules --pdb %s" % p1) + child = pytester.spawn_pytest("--doctest-modules --pdb %s" % p1) child.expect("Pdb") assert "UNEXPECTED EXCEPTION: AssertionError()" in child.before.decode("utf8") @@ -496,8 +508,8 @@ def function_1(): assert "1 failed" in rest self.flush(child) - def test_doctest_set_trace_quit(self, testdir): - p1 = testdir.makepyfile( + def test_doctest_set_trace_quit(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def function_1(): ''' @@ -507,7 +519,7 @@ def function_1(): ) # NOTE: does not use pytest.set_trace, but Python's patched pdb, # therefore "-s" is required. - child = testdir.spawn_pytest("--doctest-modules --pdb -s %s" % p1) + child = pytester.spawn_pytest("--doctest-modules --pdb -s %s" % p1) child.expect("Pdb") child.sendline("q") rest = child.read().decode("utf8") @@ -517,8 +529,8 @@ def function_1(): assert "BdbQuit" not in rest assert "UNEXPECTED EXCEPTION" not in rest - def test_pdb_interaction_capturing_twice(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_interaction_capturing_twice(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest def test_1(): @@ -532,7 +544,7 @@ def test_1(): assert 0 """ ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect(r"PDB set_trace \(IO-capturing turned off\)") child.expect("test_1") child.expect("x = 3") @@ -552,11 +564,11 @@ def test_1(): assert "1 failed" in rest self.flush(child) - def test_pdb_with_injected_do_debug(self, testdir): + def test_pdb_with_injected_do_debug(self, pytester: Pytester) -> None: """Simulates pdbpp, which injects Pdb into do_debug, and uses self.__class__ in do_continue. """ - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( mytest=""" import pdb import pytest @@ -598,7 +610,7 @@ def test_1(): pytest.fail("expected_failure") """ ) - child = testdir.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1)) + child = pytester.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1)) child.expect(r"PDB set_trace \(IO-capturing turned off\)") child.expect(r"\n\(Pdb") child.sendline("debug foo()") @@ -627,15 +639,15 @@ def test_1(): assert "AssertionError: unexpected_failure" not in rest self.flush(child) - def test_pdb_without_capture(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_without_capture(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest def test_1(): pytest.set_trace() """ ) - child = testdir.spawn_pytest("-s %s" % p1) + child = pytester.spawn_pytest("-s %s" % p1) child.expect(r">>> PDB set_trace >>>") child.expect("Pdb") child.sendline("c") @@ -644,13 +656,15 @@ def test_1(): self.flush(child) @pytest.mark.parametrize("capture_arg", ("", "-s", "-p no:capture")) - def test_pdb_continue_with_recursive_debug(self, capture_arg, testdir): + def test_pdb_continue_with_recursive_debug( + self, capture_arg, pytester: Pytester + ) -> None: """Full coverage for do_debug without capturing. This is very similar to test_pdb_interaction_continue_recursive in general, but mocks out ``pdb.set_trace`` for providing more coverage. """ - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ try: input = raw_input @@ -704,7 +718,7 @@ def do_continue(self, arg): set_trace() """ ) - child = testdir.spawn_pytest(f"--tb=short {p1} {capture_arg}") + child = pytester.spawn_pytest(f"--tb=short {p1} {capture_arg}") child.expect("=== SET_TRACE ===") before = child.before.decode("utf8") if not capture_arg: @@ -734,22 +748,22 @@ def do_continue(self, arg): assert "> PDB continue >" in rest assert "= 1 passed in" in rest - def test_pdb_used_outside_test(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_used_outside_test(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest pytest.set_trace() x = 5 """ ) - child = testdir.spawn(f"{sys.executable} {p1}") + child = pytester.spawn(f"{sys.executable} {p1}") child.expect("x = 5") child.expect("Pdb") child.sendeof() self.flush(child) - def test_pdb_used_in_generate_tests(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_used_in_generate_tests(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pytest def pytest_generate_tests(metafunc): @@ -759,22 +773,24 @@ def test_foo(a): pass """ ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect("x = 5") child.expect("Pdb") child.sendeof() self.flush(child) - def test_pdb_collection_failure_is_shown(self, testdir): - p1 = testdir.makepyfile("xxx") - result = testdir.runpytest_subprocess("--pdb", p1) + def test_pdb_collection_failure_is_shown(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile("xxx") + result = pytester.runpytest_subprocess("--pdb", p1) result.stdout.fnmatch_lines( ["E NameError: *xxx*", "*! *Exit: Quitting debugger !*"] # due to EOF ) @pytest.mark.parametrize("post_mortem", (False, True)) - def test_enter_leave_pdb_hooks_are_called(self, post_mortem, testdir): - testdir.makeconftest( + def test_enter_leave_pdb_hooks_are_called( + self, post_mortem, pytester: Pytester + ) -> None: + pytester.makeconftest( """ mypdb = None @@ -798,7 +814,7 @@ def pytest_leave_pdb(config, pdb): assert mypdb.set_attribute == "bar" """ ) - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import pytest @@ -811,9 +827,9 @@ def test_post_mortem(): """ ) if post_mortem: - child = testdir.spawn_pytest(str(p1) + " --pdb -s -k test_post_mortem") + child = pytester.spawn_pytest(str(p1) + " --pdb -s -k test_post_mortem") else: - child = testdir.spawn_pytest(str(p1) + " -k test_set_trace") + child = pytester.spawn_pytest(str(p1) + " -k test_set_trace") child.expect("enter_pdb_hook") child.sendline("c") if post_mortem: @@ -826,14 +842,18 @@ def test_post_mortem(): assert "1 failed" in rest self.flush(child) - def test_pdb_custom_cls(self, testdir, custom_pdb_calls): - p1 = testdir.makepyfile("""xxx """) - result = testdir.runpytest_inprocess("--pdb", "--pdbcls=_pytest:_CustomPdb", p1) + def test_pdb_custom_cls( + self, pytester: Pytester, custom_pdb_calls: List[str] + ) -> None: + p1 = pytester.makepyfile("""xxx """) + result = pytester.runpytest_inprocess( + "--pdb", "--pdbcls=_pytest:_CustomPdb", p1 + ) result.stdout.fnmatch_lines(["*NameError*xxx*", "*1 error*"]) assert custom_pdb_calls == ["init", "reset", "interaction"] - def test_pdb_custom_cls_invalid(self, testdir): - result = testdir.runpytest_inprocess("--pdbcls=invalid") + def test_pdb_custom_cls_invalid(self, pytester: Pytester) -> None: + result = pytester.runpytest_inprocess("--pdbcls=invalid") result.stderr.fnmatch_lines( [ "*: error: argument --pdbcls: 'invalid' is not in the format 'modname:classname'" @@ -848,14 +868,18 @@ def test_pdb_validate_usepdb_cls(self): assert _validate_usepdb_cls("pdb:DoesNotExist") == ("pdb", "DoesNotExist") - def test_pdb_custom_cls_without_pdb(self, testdir, custom_pdb_calls): - p1 = testdir.makepyfile("""xxx """) - result = testdir.runpytest_inprocess("--pdbcls=_pytest:_CustomPdb", p1) + def test_pdb_custom_cls_without_pdb( + self, pytester: Pytester, custom_pdb_calls: List[str] + ) -> None: + p1 = pytester.makepyfile("""xxx """) + result = pytester.runpytest_inprocess("--pdbcls=_pytest:_CustomPdb", p1) result.stdout.fnmatch_lines(["*NameError*xxx*", "*1 error*"]) assert custom_pdb_calls == [] - def test_pdb_custom_cls_with_set_trace(self, testdir, monkeypatch): - testdir.makepyfile( + def test_pdb_custom_cls_with_set_trace( + self, pytester: Pytester, monkeypatch: MonkeyPatch, + ) -> None: + pytester.makepyfile( custom_pdb=""" class CustomPdb(object): def __init__(self, *args, **kwargs): @@ -868,7 +892,7 @@ def set_trace(*args, **kwargs): print('custom set_trace>') """ ) - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import pytest @@ -876,8 +900,8 @@ def test_foo(): pytest.set_trace(skip=['foo.*']) """ ) - monkeypatch.setenv("PYTHONPATH", str(testdir.tmpdir)) - child = testdir.spawn_pytest("--pdbcls=custom_pdb:CustomPdb %s" % str(p1)) + monkeypatch.setenv("PYTHONPATH", str(pytester.path)) + child = pytester.spawn_pytest("--pdbcls=custom_pdb:CustomPdb %s" % str(p1)) child.expect("__init__") child.expect("custom set_trace>") @@ -885,7 +909,7 @@ def test_foo(): class TestDebuggingBreakpoints: - def test_supports_breakpoint_module_global(self): + def test_supports_breakpoint_module_global(self) -> None: """Test that supports breakpoint global marks on Python 3.7+.""" if sys.version_info >= (3, 7): assert SUPPORTS_BREAKPOINT_BUILTIN is True @@ -894,12 +918,14 @@ def test_supports_breakpoint_module_global(self): not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin" ) @pytest.mark.parametrize("arg", ["--pdb", ""]) - def test_sys_breakpointhook_configure_and_unconfigure(self, testdir, arg): + def test_sys_breakpointhook_configure_and_unconfigure( + self, pytester: Pytester, arg: str + ) -> None: """ Test that sys.breakpointhook is set to the custom Pdb class once configured, test that hook is reset to system value once pytest has been unconfigured """ - testdir.makeconftest( + pytester.makeconftest( """ import sys from pytest import hookimpl @@ -915,26 +941,26 @@ def test_check(): assert sys.breakpointhook == pytestPDB.set_trace """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_nothing(): pass """ ) args = (arg,) if arg else () - result = testdir.runpytest_subprocess(*args) + result = pytester.runpytest_subprocess(*args) result.stdout.fnmatch_lines(["*1 passed in *"]) @pytest.mark.skipif( not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin" ) - def test_pdb_custom_cls(self, testdir, custom_debugger_hook): - p1 = testdir.makepyfile( + def test_pdb_custom_cls(self, pytester: Pytester, custom_debugger_hook) -> None: + p1 = pytester.makepyfile( """ def test_nothing(): breakpoint() """ ) - result = testdir.runpytest_inprocess( + result = pytester.runpytest_inprocess( "--pdb", "--pdbcls=_pytest:_CustomDebugger", p1 ) result.stdout.fnmatch_lines(["*CustomDebugger*", "*1 passed*"]) @@ -944,8 +970,10 @@ def test_nothing(): @pytest.mark.skipif( not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin" ) - def test_environ_custom_class(self, testdir, custom_debugger_hook, arg): - testdir.makeconftest( + def test_environ_custom_class( + self, pytester: Pytester, custom_debugger_hook, arg: str + ) -> None: + pytester.makeconftest( """ import os import sys @@ -963,13 +991,13 @@ def test_check(): assert sys.breakpointhook is _pytest._CustomDebugger.set_trace """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_nothing(): pass """ ) args = (arg,) if arg else () - result = testdir.runpytest_subprocess(*args) + result = pytester.runpytest_subprocess(*args) result.stdout.fnmatch_lines(["*1 passed in *"]) @pytest.mark.skipif( @@ -979,14 +1007,14 @@ def test_nothing(): pass not _ENVIRON_PYTHONBREAKPOINT == "", reason="Requires breakpoint() default value", ) - def test_sys_breakpoint_interception(self, testdir): - p1 = testdir.makepyfile( + def test_sys_breakpoint_interception(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def test_1(): breakpoint() """ ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect("test_1") child.expect("Pdb") child.sendline("quit") @@ -998,8 +1026,8 @@ def test_1(): @pytest.mark.skipif( not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin" ) - def test_pdb_not_altered(self, testdir): - p1 = testdir.makepyfile( + def test_pdb_not_altered(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ import pdb def test_1(): @@ -1007,7 +1035,7 @@ def test_1(): assert 0 """ ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect("test_1") child.expect("Pdb") child.sendline("c") @@ -1018,8 +1046,8 @@ def test_1(): class TestTraceOption: - def test_trace_sets_breakpoint(self, testdir): - p1 = testdir.makepyfile( + def test_trace_sets_breakpoint(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def test_1(): assert True @@ -1031,7 +1059,7 @@ def test_3(): pass """ ) - child = testdir.spawn_pytest("--trace " + str(p1)) + child = pytester.spawn_pytest("--trace " + str(p1)) child.expect("test_1") child.expect("Pdb") child.sendline("c") @@ -1049,8 +1077,10 @@ def test_3(): assert "Exit: Quitting debugger" not in child.before.decode("utf8") TestPDB.flush(child) - def test_trace_with_parametrize_handles_shared_fixtureinfo(self, testdir): - p1 = testdir.makepyfile( + def test_trace_with_parametrize_handles_shared_fixtureinfo( + self, pytester: Pytester + ) -> None: + p1 = pytester.makepyfile( """ import pytest @pytest.mark.parametrize('myparam', [1,2]) @@ -1068,7 +1098,7 @@ def test_func_kw(myparam, request, func="func_kw"): assert request.function.__name__ == "test_func_kw" """ ) - child = testdir.spawn_pytest("--trace " + str(p1)) + child = pytester.spawn_pytest("--trace " + str(p1)) for func, argname in [ ("test_1", "myparam"), ("test_func", "func"), @@ -1095,16 +1125,16 @@ def test_func_kw(myparam, request, func="func_kw"): TestPDB.flush(child) -def test_trace_after_runpytest(testdir): +def test_trace_after_runpytest(pytester: Pytester) -> None: """Test that debugging's pytest_configure is re-entrant.""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ from _pytest.debugging import pytestPDB - def test_outer(testdir): + def test_outer(pytester) -> None: assert len(pytestPDB._saved) == 1 - testdir.makepyfile( + pytester.makepyfile( \""" from _pytest.debugging import pytestPDB @@ -1115,20 +1145,20 @@ def test_inner(): \""" ) - result = testdir.runpytest("-s", "-k", "test_inner") + result = pytester.runpytest("-s", "-k", "test_inner") assert result.ret == 0 assert len(pytestPDB._saved) == 1 """ ) - result = testdir.runpytest_subprocess("-s", "-p", "pytester", str(p1)) + result = pytester.runpytest_subprocess("-s", "-p", "pytester", str(p1)) result.stdout.fnmatch_lines(["test_inner_end"]) assert result.ret == 0 -def test_quit_with_swallowed_SystemExit(testdir): +def test_quit_with_swallowed_SystemExit(pytester: Pytester) -> None: """Test that debugging's pytest_configure is re-entrant.""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ def call_pdb_set_trace(): __import__('pdb').set_trace() @@ -1145,7 +1175,7 @@ def test_2(): pass """ ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) child.expect("Pdb") child.sendline("q") child.expect_exact("Exit: Quitting debugger") @@ -1155,9 +1185,9 @@ def test_2(): @pytest.mark.parametrize("fixture", ("capfd", "capsys")) -def test_pdb_suspends_fixture_capturing(testdir, fixture): +def test_pdb_suspends_fixture_capturing(pytester: Pytester, fixture: str) -> None: """Using "-s" with pytest should suspend/resume fixture capturing.""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ def test_inner({fixture}): import sys @@ -1178,7 +1208,7 @@ def test_inner({fixture}): ) ) - child = testdir.spawn_pytest(str(p1) + " -s") + child = pytester.spawn_pytest(str(p1) + " -s") child.expect("Pdb") before = child.before.decode("utf8") @@ -1203,9 +1233,9 @@ def test_inner({fixture}): assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest -def test_pdbcls_via_local_module(testdir): +def test_pdbcls_via_local_module(pytester: Pytester) -> None: """It should be imported in pytest_configure or later only.""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ def test(): print("before_set_trace") @@ -1221,7 +1251,7 @@ def runcall(self, *args, **kwds): print("runcall_called", args, kwds) """, ) - result = testdir.runpytest( + result = pytester.runpytest( str(p1), "--pdbcls=really.invalid:Value", syspathinsert=True ) result.stdout.fnmatch_lines( @@ -1232,24 +1262,24 @@ def runcall(self, *args, **kwds): ) assert result.ret == 1 - result = testdir.runpytest( + result = pytester.runpytest( str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", syspathinsert=True ) assert result.ret == 0 result.stdout.fnmatch_lines(["*set_trace_called*", "* 1 passed in *"]) # Ensure that it also works with --trace. - result = testdir.runpytest( + result = pytester.runpytest( str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", "--trace", syspathinsert=True ) assert result.ret == 0 result.stdout.fnmatch_lines(["*runcall_called*", "* 1 passed in *"]) -def test_raises_bdbquit_with_eoferror(testdir): +def test_raises_bdbquit_with_eoferror(pytester: Pytester) -> None: """It is not guaranteed that DontReadFromInput's read is called.""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ def input_without_read(*args, **kwargs): raise EOFError() @@ -1260,13 +1290,13 @@ def test(monkeypatch): __import__('pdb').set_trace() """ ) - result = testdir.runpytest(str(p1)) + result = pytester.runpytest(str(p1)) result.stdout.fnmatch_lines(["E *BdbQuit", "*= 1 failed in*"]) assert result.ret == 1 -def test_pdb_wrapper_class_is_reused(testdir): - p1 = testdir.makepyfile( +def test_pdb_wrapper_class_is_reused(pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def test(): __import__("pdb").set_trace() @@ -1288,7 +1318,7 @@ def set_trace(self, *args): print("set_trace_called", args) """, ) - result = testdir.runpytest(str(p1), "--pdbcls=mypdb:MyPdb", syspathinsert=True) + result = pytester.runpytest(str(p1), "--pdbcls=mypdb:MyPdb", syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines( ["*set_trace_called*", "*set_trace_called*", "* 1 passed in *"] diff --git a/testing/test_pytester.py b/testing/test_pytester.py index fed201dafe5..457a62dd396 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -13,6 +13,7 @@ from _pytest.pytester import CwdSnapshot from _pytest.pytester import HookRecorder from _pytest.pytester import LineMatcher +from _pytest.pytester import Pytester from _pytest.pytester import SysModulesSnapshot from _pytest.pytester import SysPathsSnapshot from _pytest.pytester import Testdir @@ -707,13 +708,13 @@ def test_inner(testdir): assert result.ret == 0 -def test_spawn_uses_tmphome(testdir) -> None: - tmphome = str(testdir.tmpdir) +def test_spawn_uses_tmphome(pytester: Pytester) -> None: + tmphome = str(pytester.path) assert os.environ.get("HOME") == tmphome - testdir.monkeypatch.setenv("CUSTOMENV", "42") + pytester._monkeypatch.setenv("CUSTOMENV", "42") - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import os @@ -724,7 +725,7 @@ def test(): tmphome=tmphome ) ) - child = testdir.spawn_pytest(str(p1)) + child = pytester.spawn_pytest(str(p1)) out = child.read() assert child.wait() == 0, out.decode("utf8") diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 490f2df09f9..77bd2ace64d 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -133,23 +133,23 @@ def test_show_runtest_logstart(self, testdir, linecomp): ) linecomp.assert_contains_lines(["*test_show_runtest_logstart.py*"]) - def test_runtest_location_shown_before_test_starts(self, testdir): - testdir.makepyfile( + def test_runtest_location_shown_before_test_starts(self, pytester): + pytester.makepyfile( """ def test_1(): import time time.sleep(20) """ ) - child = testdir.spawn_pytest("") + child = pytester.spawn_pytest("") child.expect(".*test_runtest_location.*py") child.sendeof() child.kill(15) - def test_report_collect_after_half_a_second(self, testdir): + def test_report_collect_after_half_a_second(self, pytester, monkeypatch): """Test for "collecting" being updated after 0.5s""" - testdir.makepyfile( + pytester.makepyfile( **{ "test1.py": """ import _pytest.terminal @@ -163,9 +163,9 @@ def test_1(): } ) # Explicitly test colored output. - testdir.monkeypatch.setenv("PY_COLORS", "1") + monkeypatch.setenv("PY_COLORS", "1") - child = testdir.spawn_pytest("-v test1.py test2.py") + child = pytester.spawn_pytest("-v test1.py test2.py") child.expect(r"collecting \.\.\.") child.expect(r"collecting 1 item") child.expect(r"collecting 2 items") From ca82214444b82675af0a5ef2b4696161be0e5a0c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 25 Oct 2020 03:44:04 +0200 Subject: [PATCH 720/823] pytester: workaround issue causing spawn to crash or hang In pytester tests, pytest stashes & restores the sys.modules for each test. So if the test imports a new module, it is initialized anew each time. Turns out the readline module isn't multi-init safe, which causes pytester.spawn to crash or hang. So preserve it as a workaround. --- changelog/7913.bugfix.rst | 1 + src/_pytest/pytester.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog/7913.bugfix.rst diff --git a/changelog/7913.bugfix.rst b/changelog/7913.bugfix.rst new file mode 100644 index 00000000000..e8c4613bf4c --- /dev/null +++ b/changelog/7913.bugfix.rst @@ -0,0 +1 @@ +Fixed a crash or hang in ``pytester.spawn`` when the ``readline`` module is involved. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a1768363bb3..935be84c122 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -707,8 +707,11 @@ def __take_sys_modules_snapshot(self) -> SysModulesSnapshot: # Some zope modules used by twisted-related tests keep internal state # and can't be deleted; we had some trouble in the past with # `zope.interface` for example. + # + # Preserve readline due to https://bugs.python.org/issue41033. + # pexpect issues a SIGWINCH. def preserve_module(name): - return name.startswith("zope") + return name.startswith(("zope", "readline")) return SysModulesSnapshot(preserve=preserve_module) From 1bd83e75a4cc3b89c729e3e2a4f6fbe8423c937c Mon Sep 17 00:00:00 2001 From: symonk Date: Sun, 25 Oct 2020 17:27:19 +0000 Subject: [PATCH 721/823] refactor test mark to use new pytester --- testing/test_mark.py | 341 ++++++++++++++++++++++--------------------- 1 file changed, 177 insertions(+), 164 deletions(-) diff --git a/testing/test_mark.py b/testing/test_mark.py index b3240b1b11f..44f0be90065 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1,13 +1,16 @@ import os import sys +from typing import List +from typing import Optional from unittest import mock import pytest from _pytest.config import ExitCode -from _pytest.mark import MarkGenerator as Mark +from _pytest.mark import MarkGenerator from _pytest.mark.structures import EMPTY_PARAMETERSET_OPTION from _pytest.nodes import Collector from _pytest.nodes import Node +from _pytest.pytester import Pytester class TestMark: @@ -18,7 +21,7 @@ def test_pytest_exists_in_namespace_all(self, attr: str, modulename: str) -> Non assert attr in module.__all__ # type: ignore def test_pytest_mark_notcallable(self) -> None: - mark = Mark() + mark = MarkGenerator() with pytest.raises(TypeError): mark() # type: ignore[operator] @@ -37,16 +40,16 @@ class SomeClass: assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap] def test_pytest_mark_name_starts_with_underscore(self): - mark = Mark() + mark = MarkGenerator() with pytest.raises(AttributeError): mark._some_name -def test_marked_class_run_twice(testdir): +def test_marked_class_run_twice(pytester: Pytester): """Test fails file is run twice that contains marked class. See issue#683. """ - py_file = testdir.makepyfile( + py_file = pytester.makepyfile( """ import pytest @pytest.mark.parametrize('abc', [1, 2, 3]) @@ -55,13 +58,13 @@ def test_1(self, abc): assert abc in [1, 2, 3] """ ) - file_name = os.path.basename(py_file.strpath) - rec = testdir.inline_run(file_name, file_name) + file_name = os.path.basename(py_file) + rec = pytester.inline_run(file_name, file_name) rec.assertoutcome(passed=6) -def test_ini_markers(testdir): - testdir.makeini( +def test_ini_markers(pytester): + pytester.makeini( """ [pytest] markers = @@ -69,7 +72,7 @@ def test_ini_markers(testdir): a2: this is a smoke marker """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_markers(pytestconfig): markers = pytestconfig.getini("markers") @@ -79,12 +82,12 @@ def test_markers(pytestconfig): assert markers[1].startswith("a2:") """ ) - rec = testdir.inline_run() + rec = pytester.inline_run() rec.assertoutcome(passed=1) -def test_markers_option(testdir): - testdir.makeini( +def test_markers_option(pytester): + pytester.makeini( """ [pytest] markers = @@ -93,21 +96,21 @@ def test_markers_option(testdir): nodescription """ ) - result = testdir.runpytest("--markers") + result = pytester.runpytest("--markers") result.stdout.fnmatch_lines( ["*a1*this is a webtest*", "*a1some*another marker", "*nodescription*"] ) -def test_ini_markers_whitespace(testdir): - testdir.makeini( +def test_ini_markers_whitespace(pytester): + pytester.makeini( """ [pytest] markers = a1 : this is a whitespace marker """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -116,33 +119,33 @@ def test_markers(): assert True """ ) - rec = testdir.inline_run("--strict-markers", "-m", "a1") + rec = pytester.inline_run("--strict-markers", "-m", "a1") rec.assertoutcome(passed=1) -def test_marker_without_description(testdir): - testdir.makefile( +def test_marker_without_description(pytester: Pytester): + pytester.makefile( ".cfg", setup=""" [tool:pytest] markers=slow """, ) - testdir.makeconftest( + pytester.makeconftest( """ import pytest pytest.mark.xfail('FAIL') """ ) - ftdir = testdir.mkdir("ft1_dummy") - testdir.tmpdir.join("conftest.py").move(ftdir.join("conftest.py")) - rec = testdir.runpytest("--strict-markers") + ftdir = pytester.mkdir("ft1_dummy") + pytester.path.joinpath("conftest.py").replace(ftdir.joinpath("conftest.py")) + rec = pytester.runpytest("--strict-markers") rec.assert_outcomes() -def test_markers_option_with_plugin_in_current_dir(testdir): - testdir.makeconftest('pytest_plugins = "flip_flop"') - testdir.makepyfile( +def test_markers_option_with_plugin_in_current_dir(pytester: Pytester): + pytester.makeconftest('pytest_plugins = "flip_flop"') + pytester.makepyfile( flip_flop="""\ def pytest_configure(config): config.addinivalue_line("markers", "flip:flop") @@ -154,7 +157,7 @@ def pytest_generate_tests(metafunc): return metafunc.parametrize("x", (10, 20))""" ) - testdir.makepyfile( + pytester.makepyfile( """\ import pytest @pytest.mark.flipper @@ -162,12 +165,12 @@ def test_example(x): assert x""" ) - result = testdir.runpytest("--markers") + result = pytester.runpytest("--markers") result.stdout.fnmatch_lines(["*flip*flop*"]) -def test_mark_on_pseudo_function(testdir): - testdir.makepyfile( +def test_mark_on_pseudo_function(pytester: Pytester): + pytester.makepyfile( """ import pytest @@ -176,13 +179,13 @@ def test_hello(): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @pytest.mark.parametrize("option_name", ["--strict-markers", "--strict"]) -def test_strict_prohibits_unregistered_markers(testdir, option_name): - testdir.makepyfile( +def test_strict_prohibits_unregistered_markers(pytester, option_name): + pytester.makepyfile( """ import pytest @pytest.mark.unregisteredmark @@ -190,7 +193,7 @@ def test_hello(): pass """ ) - result = testdir.runpytest(option_name) + result = pytester.runpytest(option_name) assert result.ret != 0 result.stdout.fnmatch_lines( ["'unregisteredmark' not found in `markers` configuration option"] @@ -208,8 +211,10 @@ def test_hello(): ("xyz or xyz2", ["test_one", "test_two"]), ], ) -def test_mark_option(expr: str, expected_passed: str, testdir) -> None: - testdir.makepyfile( +def test_mark_option( + expr: str, expected_passed: List[Optional[str]], pytester: Pytester +) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.xyz @@ -220,18 +225,20 @@ def test_two(): pass """ ) - rec = testdir.inline_run("-m", expr) + rec = pytester.inline_run("-m", expr) passed, skipped, fail = rec.listoutcomes() - passed = [x.nodeid.split("::")[-1] for x in passed] - assert passed == expected_passed + passed_str = [x.nodeid.split("::")[-1] for x in passed] + assert passed_str == expected_passed @pytest.mark.parametrize( ("expr", "expected_passed"), [("interface", ["test_interface"]), ("not interface", ["test_nointer"])], ) -def test_mark_option_custom(expr: str, expected_passed: str, testdir) -> None: - testdir.makeconftest( +def test_mark_option_custom( + expr: str, expected_passed: List[str], pytester: Pytester +) -> None: + pytester.makeconftest( """ import pytest def pytest_collection_modifyitems(items): @@ -240,7 +247,7 @@ def pytest_collection_modifyitems(items): item.add_marker(pytest.mark.interface) """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_interface(): pass @@ -248,10 +255,10 @@ def test_nointer(): pass """ ) - rec = testdir.inline_run("-m", expr) + rec = pytester.inline_run("-m", expr) passed, skipped, fail = rec.listoutcomes() - passed = [x.nodeid.split("::")[-1] for x in passed] - assert passed == expected_passed + passed_str = [x.nodeid.split("::")[-1] for x in passed] + assert passed_str == expected_passed @pytest.mark.parametrize( @@ -266,8 +273,10 @@ def test_nointer(): ("not (1 or 2)", ["test_interface", "test_nointer", "test_pass"]), ], ) -def test_keyword_option_custom(expr: str, expected_passed: str, testdir) -> None: - testdir.makepyfile( +def test_keyword_option_custom( + expr: str, expected_passed: List[str], pytester: Pytester +) -> None: + pytester.makepyfile( """ def test_interface(): pass @@ -281,15 +290,15 @@ def test_2(): pass """ ) - rec = testdir.inline_run("-k", expr) + rec = pytester.inline_run("-k", expr) passed, skipped, fail = rec.listoutcomes() - passed = [x.nodeid.split("::")[-1] for x in passed] - assert passed == expected_passed + passed_str = [x.nodeid.split("::")[-1] for x in passed] + assert passed_str == expected_passed -def test_keyword_option_considers_mark(testdir): - testdir.copy_example("marks/marks_considered_keywords") - rec = testdir.inline_run("-k", "foo") +def test_keyword_option_considers_mark(pytester: Pytester): + pytester.copy_example("marks/marks_considered_keywords") + rec = pytester.inline_run("-k", "foo") passed = rec.listoutcomes()[0] assert len(passed) == 1 @@ -302,8 +311,10 @@ def test_keyword_option_considers_mark(testdir): ("2-3", ["test_func[2-3]"]), ], ) -def test_keyword_option_parametrize(expr: str, expected_passed: str, testdir) -> None: - testdir.makepyfile( +def test_keyword_option_parametrize( + expr: str, expected_passed: List[str], pytester: Pytester +) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.parametrize("arg", [None, 1.3, "2-3"]) @@ -311,14 +322,14 @@ def test_func(arg): pass """ ) - rec = testdir.inline_run("-k", expr) + rec = pytester.inline_run("-k", expr) passed, skipped, fail = rec.listoutcomes() - passed = [x.nodeid.split("::")[-1] for x in passed] - assert passed == expected_passed + passed_str = [x.nodeid.split("::")[-1] for x in passed] + assert passed_str == expected_passed -def test_parametrize_with_module(testdir): - testdir.makepyfile( +def test_parametrize_with_module(pytester: Pytester): + pytester.makepyfile( """ import pytest @pytest.mark.parametrize("arg", [pytest,]) @@ -326,7 +337,7 @@ def test_func(arg): pass """ ) - rec = testdir.inline_run() + rec = pytester.inline_run() passed, skipped, fail = rec.listoutcomes() expected_id = "test_func[" + pytest.__name__ + "]" assert passed[0].nodeid.split("::")[-1] == expected_id @@ -356,23 +367,23 @@ def test_func(arg): ], ) def test_keyword_option_wrong_arguments( - expr: str, expected_error: str, testdir, capsys + expr: str, expected_error: str, pytester: Pytester, capsys ) -> None: - testdir.makepyfile( + pytester.makepyfile( """ def test_func(arg): pass """ ) - testdir.inline_run("-k", expr) + pytester.inline_run("-k", expr) err = capsys.readouterr().err assert expected_error in err -def test_parametrized_collected_from_command_line(testdir): +def test_parametrized_collected_from_command_line(pytester: Pytester): """Parametrized test not collected if test named specified in command line issue#649.""" - py_file = testdir.makepyfile( + py_file = pytester.makepyfile( """ import pytest @pytest.mark.parametrize("arg", [None, 1.3, "2-3"]) @@ -380,14 +391,14 @@ def test_func(arg): pass """ ) - file_name = os.path.basename(py_file.strpath) - rec = testdir.inline_run(file_name + "::" + "test_func") + file_name = os.path.basename(py_file) + rec = pytester.inline_run(file_name + "::" + "test_func") rec.assertoutcome(passed=3) -def test_parametrized_collect_with_wrong_args(testdir): +def test_parametrized_collect_with_wrong_args(pytester: Pytester): """Test collect parametrized func with wrong number of args.""" - py_file = testdir.makepyfile( + py_file = pytester.makepyfile( """ import pytest @@ -397,7 +408,7 @@ def test_func(foo, bar): """ ) - result = testdir.runpytest(py_file) + result = pytester.runpytest(py_file) result.stdout.fnmatch_lines( [ 'test_parametrized_collect_with_wrong_args.py::test_func: in "parametrize" the number of names (2):', @@ -408,9 +419,9 @@ def test_func(foo, bar): ) -def test_parametrized_with_kwargs(testdir): +def test_parametrized_with_kwargs(pytester: Pytester): """Test collect parametrized func with wrong number of args.""" - py_file = testdir.makepyfile( + py_file = pytester.makepyfile( """ import pytest @@ -424,13 +435,13 @@ def test_func(a, b): """ ) - result = testdir.runpytest(py_file) + result = pytester.runpytest(py_file) assert result.ret == 0 -def test_parametrize_iterator(testdir): +def test_parametrize_iterator(pytester: Pytester): """`parametrize` should work with generators (#5354).""" - py_file = testdir.makepyfile( + py_file = pytester.makepyfile( """\ import pytest @@ -444,16 +455,16 @@ def test(a): assert a >= 1 """ ) - result = testdir.runpytest(py_file) + result = pytester.runpytest(py_file) assert result.ret == 0 # should not skip any tests result.stdout.fnmatch_lines(["*3 passed*"]) class TestFunctional: - def test_merging_markers_deep(self, testdir): + def test_merging_markers_deep(self, pytester: Pytester): # issue 199 - propagate markers into nested classes - p = testdir.makepyfile( + p = pytester.makepyfile( """ import pytest class TestA(object): @@ -466,13 +477,15 @@ def test_d(self): assert True """ ) - items, rec = testdir.inline_genitems(p) + items, rec = pytester.inline_genitems(p) for item in items: print(item, item.keywords) assert [x for x in item.iter_markers() if x.name == "a"] - def test_mark_decorator_subclass_does_not_propagate_to_base(self, testdir): - p = testdir.makepyfile( + def test_mark_decorator_subclass_does_not_propagate_to_base( + self, pytester: Pytester + ): + p = pytester.makepyfile( """ import pytest @@ -487,12 +500,12 @@ class Test2(Base): def test_bar(self): pass """ ) - items, rec = testdir.inline_genitems(p) + items, rec = pytester.inline_genitems(p) self.assert_markers(items, test_foo=("a", "b"), test_bar=("a",)) - def test_mark_should_not_pass_to_siebling_class(self, testdir): + def test_mark_should_not_pass_to_siebling_class(self, pytester: Pytester): """#568""" - p = testdir.makepyfile( + p = pytester.makepyfile( """ import pytest @@ -510,7 +523,7 @@ class TestOtherSub(TestBase): """ ) - items, rec = testdir.inline_genitems(p) + items, rec = pytester.inline_genitems(p) base_item, sub_item, sub_item_other = items print(items, [x.nodeid for x in items]) # new api segregates @@ -518,8 +531,8 @@ class TestOtherSub(TestBase): assert not list(sub_item_other.iter_markers(name="b")) assert list(sub_item.iter_markers(name="b")) - def test_mark_decorator_baseclasses_merged(self, testdir): - p = testdir.makepyfile( + def test_mark_decorator_baseclasses_merged(self, pytester: Pytester): + p = pytester.makepyfile( """ import pytest @@ -538,11 +551,11 @@ class Test2(Base2): def test_bar(self): pass """ ) - items, rec = testdir.inline_genitems(p) + items, rec = pytester.inline_genitems(p) self.assert_markers(items, test_foo=("a", "b", "c"), test_bar=("a", "b", "d")) - def test_mark_closest(self, testdir): - p = testdir.makepyfile( + def test_mark_closest(self, pytester: Pytester): + p = pytester.makepyfile( """ import pytest @@ -557,14 +570,14 @@ def test_has_inherited(self): """ ) - items, rec = testdir.inline_genitems(p) + items, rec = pytester.inline_genitems(p) has_own, has_inherited = items - assert has_own.get_closest_marker("c").kwargs == {"location": "function"} - assert has_inherited.get_closest_marker("c").kwargs == {"location": "class"} + assert has_own.get_closest_marker("c").kwargs == {"location": "function"} # type: ignore + assert has_inherited.get_closest_marker("c").kwargs == {"location": "class"} # type: ignore assert has_own.get_closest_marker("missing") is None - def test_mark_with_wrong_marker(self, testdir): - reprec = testdir.inline_runsource( + def test_mark_with_wrong_marker(self, pytester: Pytester): + reprec = pytester.inline_runsource( """ import pytest class pytestmark(object): @@ -577,8 +590,8 @@ def test_func(): assert len(values) == 1 assert "TypeError" in str(values[0].longrepr) - def test_mark_dynamically_in_funcarg(self, testdir): - testdir.makeconftest( + def test_mark_dynamically_in_funcarg(self, pytester: Pytester): + pytester.makeconftest( """ import pytest @pytest.fixture @@ -589,17 +602,17 @@ def pytest_terminal_summary(terminalreporter): terminalreporter._tw.line("keyword: %s" % values[0].keywords) """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_func(arg): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["keyword: *hello*"]) - def test_no_marker_match_on_unmarked_names(self, testdir): - p = testdir.makepyfile( + def test_no_marker_match_on_unmarked_names(self, pytester: Pytester): + p = pytester.makepyfile( """ import pytest @pytest.mark.shouldmatch @@ -610,15 +623,15 @@ def test_unmarked(): assert 1 """ ) - reprec = testdir.inline_run("-m", "test_unmarked", p) + reprec = pytester.inline_run("-m", "test_unmarked", p) passed, skipped, failed = reprec.listoutcomes() assert len(passed) + len(skipped) + len(failed) == 0 dlist = reprec.getcalls("pytest_deselected") deselected_tests = dlist[0].items assert len(deselected_tests) == 2 - def test_keywords_at_node_level(self, testdir): - testdir.makepyfile( + def test_keywords_at_node_level(self, pytester: Pytester): + pytester.makepyfile( """ import pytest @pytest.fixture(scope="session", autouse=True) @@ -636,11 +649,11 @@ def test_function(): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_keyword_added_for_session(self, testdir): - testdir.makeconftest( + def test_keyword_added_for_session(self, pytester: Pytester): + pytester.makeconftest( """ import pytest def pytest_collection_modifyitems(session): @@ -651,7 +664,7 @@ def pytest_collection_modifyitems(session): session.add_marker(10)) """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_some(request): assert "mark1" in request.keywords @@ -664,14 +677,14 @@ def test_some(request): assert marker.kwargs == {} """ ) - reprec = testdir.inline_run("-m", "mark1") + reprec = pytester.inline_run("-m", "mark1") reprec.assertoutcome(passed=1) def assert_markers(self, items, **expected): """Assert that given items have expected marker names applied to them. expected should be a dict of (item name -> seq of expected marker names). - Note: this could be moved to ``testdir`` if proven to be useful + Note: this could be moved to ``pytester`` if proven to be useful to other modules. """ items = {x.name: x for x in items} @@ -680,9 +693,9 @@ def assert_markers(self, items, **expected): assert markers == set(expected_markers) @pytest.mark.filterwarnings("ignore") - def test_mark_from_parameters(self, testdir): + def test_mark_from_parameters(self, pytester: Pytester): """#1540""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -701,12 +714,12 @@ def test_1(parameter): assert True """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(skipped=1) - def test_reevaluate_dynamic_expr(self, testdir): + def test_reevaluate_dynamic_expr(self, pytester: Pytester): """#7360""" - py_file1 = testdir.makepyfile( + py_file1 = pytester.makepyfile( test_reevaluate_dynamic_expr1=""" import pytest @@ -717,7 +730,7 @@ def test_should_skip(): assert True """ ) - py_file2 = testdir.makepyfile( + py_file2 = pytester.makepyfile( test_reevaluate_dynamic_expr2=""" import pytest @@ -729,15 +742,15 @@ def test_should_not_skip(): """ ) - file_name1 = os.path.basename(py_file1.strpath) - file_name2 = os.path.basename(py_file2.strpath) - reprec = testdir.inline_run(file_name1, file_name2) + file_name1 = os.path.basename(py_file1) + file_name2 = os.path.basename(py_file2) + reprec = pytester.inline_run(file_name1, file_name2) reprec.assertoutcome(passed=1, skipped=1) class TestKeywordSelection: - def test_select_simple(self, testdir): - file_test = testdir.makepyfile( + def test_select_simple(self, pytester: Pytester): + file_test = pytester.makepyfile( """ def test_one(): assert 0 @@ -748,7 +761,7 @@ def test_method_one(self): ) def check(keyword, name): - reprec = testdir.inline_run("-s", "-k", keyword, file_test) + reprec = pytester.inline_run("-s", "-k", keyword, file_test) passed, skipped, failed = reprec.listoutcomes() assert len(failed) == 1 assert failed[0].nodeid.split("::")[-1] == name @@ -769,8 +782,8 @@ def check(keyword, name): "xxx and TestClass and test_2", ], ) - def test_select_extra_keywords(self, testdir, keyword): - p = testdir.makepyfile( + def test_select_extra_keywords(self, pytester: Pytester, keyword): + p = pytester.makepyfile( test_select=""" def test_1(): pass @@ -779,7 +792,7 @@ def test_2(self): pass """ ) - testdir.makepyfile( + pytester.makepyfile( conftest=""" import pytest @pytest.hookimpl(hookwrapper=True) @@ -790,7 +803,7 @@ def pytest_pycollect_makeitem(name): item.extra_keyword_matches.add("xxx") """ ) - reprec = testdir.inline_run(p.dirpath(), "-s", "-k", keyword) + reprec = pytester.inline_run(p.parent, "-s", "-k", keyword) print("keyword", repr(keyword)) passed, skipped, failed = reprec.listoutcomes() assert len(passed) == 1 @@ -799,15 +812,15 @@ def pytest_pycollect_makeitem(name): assert len(dlist) == 1 assert dlist[0].items[0].name == "test_1" - def test_select_starton(self, testdir): - threepass = testdir.makepyfile( + def test_select_starton(self, pytester: Pytester): + threepass = pytester.makepyfile( test_threepass=""" def test_one(): assert 1 def test_two(): assert 1 def test_three(): assert 1 """ ) - reprec = testdir.inline_run("-k", "test_two:", threepass) + reprec = pytester.inline_run("-k", "test_two:", threepass) passed, skipped, failed = reprec.listoutcomes() assert len(passed) == 2 assert not failed @@ -816,21 +829,21 @@ def test_three(): assert 1 item = dlist[0].items[0] assert item.name == "test_one" - def test_keyword_extra(self, testdir): - p = testdir.makepyfile( + def test_keyword_extra(self, pytester: Pytester): + p = pytester.makepyfile( """ def test_one(): assert 0 test_one.mykeyword = True """ ) - reprec = testdir.inline_run("-k", "mykeyword", p) + reprec = pytester.inline_run("-k", "mykeyword", p) passed, skipped, failed = reprec.countoutcomes() assert failed == 1 @pytest.mark.xfail - def test_keyword_extra_dash(self, testdir): - p = testdir.makepyfile( + def test_keyword_extra_dash(self, pytester: Pytester): + p = pytester.makepyfile( """ def test_one(): assert 0 @@ -839,42 +852,42 @@ def test_one(): ) # with argparse the argument to an option cannot # start with '-' - reprec = testdir.inline_run("-k", "-mykeyword", p) + reprec = pytester.inline_run("-k", "-mykeyword", p) passed, skipped, failed = reprec.countoutcomes() assert passed + skipped + failed == 0 @pytest.mark.parametrize( "keyword", ["__", "+", ".."], ) - def test_no_magic_values(self, testdir, keyword: str) -> None: + def test_no_magic_values(self, pytester: Pytester, keyword: str) -> None: """Make sure the tests do not match on magic values, no double underscored values, like '__dict__' and '+'. """ - p = testdir.makepyfile( + p = pytester.makepyfile( """ def test_one(): assert 1 """ ) - reprec = testdir.inline_run("-k", keyword, p) + reprec = pytester.inline_run("-k", keyword, p) passed, skipped, failed = reprec.countoutcomes() dlist = reprec.getcalls("pytest_deselected") assert passed + skipped + failed == 0 deselected_tests = dlist[0].items assert len(deselected_tests) == 1 - def test_no_match_directories_outside_the_suite(self, testdir): + def test_no_match_directories_outside_the_suite(self, pytester: Pytester): """`-k` should not match against directories containing the test suite (#7040).""" test_contents = """ def test_aaa(): pass def test_ddd(): pass """ - testdir.makepyfile( + pytester.makepyfile( **{"ddd/tests/__init__.py": "", "ddd/tests/test_foo.py": test_contents} ) def get_collected_names(*args): - _, rec = testdir.inline_genitems(*args) + _, rec = pytester.inline_genitems(*args) calls = rec.getcalls("pytest_collection_finish") assert len(calls) == 1 return [x.name for x in calls[0].session.items] @@ -883,7 +896,7 @@ def get_collected_names(*args): assert get_collected_names() == ["test_aaa", "test_ddd"] # do not collect anything based on names outside the collection tree - assert get_collected_names("-k", testdir.tmpdir.basename) == [] + assert get_collected_names("-k", pytester._name) == [] # "-k ddd" should only collect "test_ddd", but not # 'test_aaa' just because one of its parent directories is named "ddd"; @@ -913,9 +926,9 @@ def test_aliases(self) -> None: @pytest.mark.parametrize("mark", [None, "", "skip", "xfail"]) -def test_parameterset_for_parametrize_marks(testdir, mark): +def test_parameterset_for_parametrize_marks(pytester: Pytester, mark): if mark is not None: - testdir.makeini( + pytester.makeini( """ [pytest] {}={} @@ -924,7 +937,7 @@ def test_parameterset_for_parametrize_marks(testdir, mark): ) ) - config = testdir.parseconfig() + config = pytester.parseconfig() from _pytest.mark import pytest_configure, get_empty_parameterset_mark pytest_configure(config) @@ -938,8 +951,8 @@ def test_parameterset_for_parametrize_marks(testdir, mark): assert result_mark.kwargs.get("run") is False -def test_parameterset_for_fail_at_collect(testdir): - testdir.makeini( +def test_parameterset_for_fail_at_collect(pytester: Pytester): + pytester.makeini( """ [pytest] {}=fail_at_collect @@ -948,7 +961,7 @@ def test_parameterset_for_fail_at_collect(testdir): ) ) - config = testdir.parseconfig() + config = pytester.parseconfig() from _pytest.mark import pytest_configure, get_empty_parameterset_mark pytest_configure(config) @@ -959,7 +972,7 @@ def test_parameterset_for_fail_at_collect(testdir): ): get_empty_parameterset_mark(config, ["a"], pytest_configure) - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ import pytest @@ -968,7 +981,7 @@ def test(): pass """ ) - result = testdir.runpytest(str(p1)) + result = pytester.runpytest(str(p1)) result.stdout.fnmatch_lines( [ "collected 0 items / 1 error", @@ -980,13 +993,13 @@ def test(): assert result.ret == ExitCode.INTERRUPTED -def test_parameterset_for_parametrize_bad_markname(testdir): +def test_parameterset_for_parametrize_bad_markname(pytester: Pytester): with pytest.raises(pytest.UsageError): - test_parameterset_for_parametrize_marks(testdir, "bad") + test_parameterset_for_parametrize_marks(pytester, "bad") -def test_mark_expressions_no_smear(testdir): - testdir.makepyfile( +def test_mark_expressions_no_smear(pytester: Pytester): + pytester.makepyfile( """ import pytest @@ -1004,7 +1017,7 @@ class TestBarClass(BaseTests): """ ) - reprec = testdir.inline_run("-m", "FOO") + reprec = pytester.inline_run("-m", "FOO") passed, skipped, failed = reprec.countoutcomes() dlist = reprec.getcalls("pytest_deselected") assert passed == 1 @@ -1014,7 +1027,7 @@ class TestBarClass(BaseTests): # todo: fixed # keywords smear - expected behaviour - # reprec_keywords = testdir.inline_run("-k", "FOO") + # reprec_keywords = pytester.inline_run("-k", "FOO") # passed_k, skipped_k, failed_k = reprec_keywords.countoutcomes() # assert passed_k == 2 # assert skipped_k == failed_k == 0 @@ -1034,9 +1047,9 @@ def test_addmarker_order(): @pytest.mark.filterwarnings("ignore") -def test_markers_from_parametrize(testdir): +def test_markers_from_parametrize(pytester: Pytester): """#3605""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1067,7 +1080,7 @@ def test_custom_mark_parametrized(obj_type): """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=4) @@ -1084,8 +1097,8 @@ def test_pytest_param_id_allows_none_or_string(s): @pytest.mark.parametrize("expr", ("NOT internal_err", "NOT (internal_err)", "bogus/")) -def test_marker_expr_eval_failure_handling(testdir, expr): - foo = testdir.makepyfile( +def test_marker_expr_eval_failure_handling(pytester: Pytester, expr): + foo = pytester.makepyfile( """ import pytest @@ -1095,6 +1108,6 @@ def test_foo(): """ ) expected = f"ERROR: Wrong expression passed to '-m': {expr}: *" - result = testdir.runpytest(foo, "-m", expr) + result = pytester.runpytest(foo, "-m", expr) result.stderr.fnmatch_lines([expected]) assert result.ret == ExitCode.USAGE_ERROR From 7495d2c345d0f9dff9452d229ac89c0741833cc2 Mon Sep 17 00:00:00 2001 From: symonk Date: Sun, 25 Oct 2020 17:33:40 +0000 Subject: [PATCH 722/823] add missing pytester type hints --- testing/test_mark.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/test_mark.py b/testing/test_mark.py index 44f0be90065..9cab182ae89 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -63,7 +63,7 @@ def test_1(self, abc): rec.assertoutcome(passed=6) -def test_ini_markers(pytester): +def test_ini_markers(pytester: Pytester): pytester.makeini( """ [pytest] @@ -86,7 +86,7 @@ def test_markers(pytestconfig): rec.assertoutcome(passed=1) -def test_markers_option(pytester): +def test_markers_option(pytester: Pytester): pytester.makeini( """ [pytest] @@ -102,7 +102,7 @@ def test_markers_option(pytester): ) -def test_ini_markers_whitespace(pytester): +def test_ini_markers_whitespace(pytester: Pytester): pytester.makeini( """ [pytest] @@ -184,7 +184,7 @@ def test_hello(): @pytest.mark.parametrize("option_name", ["--strict-markers", "--strict"]) -def test_strict_prohibits_unregistered_markers(pytester, option_name): +def test_strict_prohibits_unregistered_markers(pytester: Pytester, option_name): pytester.makepyfile( """ import pytest From 6b7203aba72b86421d7e844334bbdc4caebcd045 Mon Sep 17 00:00:00 2001 From: symonk Date: Sun, 25 Oct 2020 17:38:12 +0000 Subject: [PATCH 723/823] add conversion for test_warning_types.py also --- testing/test_warning_types.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/testing/test_warning_types.py b/testing/test_warning_types.py index f16d7252a68..46bcee364fc 100644 --- a/testing/test_warning_types.py +++ b/testing/test_warning_types.py @@ -2,6 +2,7 @@ import _pytest.warning_types import pytest +from _pytest.pytester import Pytester @pytest.mark.parametrize( @@ -20,11 +21,11 @@ def test_warning_types(warning_class): @pytest.mark.filterwarnings("error::pytest.PytestWarning") -def test_pytest_warnings_repr_integration_test(testdir): +def test_pytest_warnings_repr_integration_test(pytester: Pytester): """Small integration test to ensure our small hack of setting the __module__ attribute of our warnings actually works (#5452). """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest import warnings @@ -33,5 +34,5 @@ def test(): warnings.warn(pytest.PytestWarning("some warning")) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["E pytest.PytestWarning: some warning"]) From c818ac2248c31ba3f2e9d4b84ede19c1ab1bbbbb Mon Sep 17 00:00:00 2001 From: symonk Date: Sun, 25 Oct 2020 18:03:59 +0000 Subject: [PATCH 724/823] Tidy up type hints for pytest in test_marks & test_warning_types --- testing/test_mark.py | 92 ++++++++++++++++++----------------- testing/test_warning_types.py | 4 +- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/testing/test_mark.py b/testing/test_mark.py index 9cab182ae89..bc538fe5db2 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -39,13 +39,13 @@ class SomeClass: assert pytest.mark.foo(SomeClass) is SomeClass assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap] - def test_pytest_mark_name_starts_with_underscore(self): + def test_pytest_mark_name_starts_with_underscore(self) -> None: mark = MarkGenerator() with pytest.raises(AttributeError): mark._some_name -def test_marked_class_run_twice(pytester: Pytester): +def test_marked_class_run_twice(pytester: Pytester) -> None: """Test fails file is run twice that contains marked class. See issue#683. """ @@ -63,7 +63,7 @@ def test_1(self, abc): rec.assertoutcome(passed=6) -def test_ini_markers(pytester: Pytester): +def test_ini_markers(pytester: Pytester) -> None: pytester.makeini( """ [pytest] @@ -86,7 +86,7 @@ def test_markers(pytestconfig): rec.assertoutcome(passed=1) -def test_markers_option(pytester: Pytester): +def test_markers_option(pytester: Pytester) -> None: pytester.makeini( """ [pytest] @@ -102,7 +102,7 @@ def test_markers_option(pytester: Pytester): ) -def test_ini_markers_whitespace(pytester: Pytester): +def test_ini_markers_whitespace(pytester: Pytester) -> None: pytester.makeini( """ [pytest] @@ -123,7 +123,7 @@ def test_markers(): rec.assertoutcome(passed=1) -def test_marker_without_description(pytester: Pytester): +def test_marker_without_description(pytester: Pytester) -> None: pytester.makefile( ".cfg", setup=""" @@ -143,7 +143,7 @@ def test_marker_without_description(pytester: Pytester): rec.assert_outcomes() -def test_markers_option_with_plugin_in_current_dir(pytester: Pytester): +def test_markers_option_with_plugin_in_current_dir(pytester: Pytester) -> None: pytester.makeconftest('pytest_plugins = "flip_flop"') pytester.makepyfile( flip_flop="""\ @@ -169,7 +169,7 @@ def test_example(x): result.stdout.fnmatch_lines(["*flip*flop*"]) -def test_mark_on_pseudo_function(pytester: Pytester): +def test_mark_on_pseudo_function(pytester: Pytester) -> None: pytester.makepyfile( """ import pytest @@ -184,7 +184,7 @@ def test_hello(): @pytest.mark.parametrize("option_name", ["--strict-markers", "--strict"]) -def test_strict_prohibits_unregistered_markers(pytester: Pytester, option_name): +def test_strict_prohibits_unregistered_markers(pytester: Pytester, option_name) -> None: pytester.makepyfile( """ import pytest @@ -296,7 +296,7 @@ def test_2(): assert passed_str == expected_passed -def test_keyword_option_considers_mark(pytester: Pytester): +def test_keyword_option_considers_mark(pytester: Pytester) -> None: pytester.copy_example("marks/marks_considered_keywords") rec = pytester.inline_run("-k", "foo") passed = rec.listoutcomes()[0] @@ -328,7 +328,7 @@ def test_func(arg): assert passed_str == expected_passed -def test_parametrize_with_module(pytester: Pytester): +def test_parametrize_with_module(pytester: Pytester) -> None: pytester.makepyfile( """ import pytest @@ -380,7 +380,7 @@ def test_func(arg): assert expected_error in err -def test_parametrized_collected_from_command_line(pytester: Pytester): +def test_parametrized_collected_from_command_line(pytester: Pytester) -> None: """Parametrized test not collected if test named specified in command line issue#649.""" py_file = pytester.makepyfile( @@ -396,7 +396,7 @@ def test_func(arg): rec.assertoutcome(passed=3) -def test_parametrized_collect_with_wrong_args(pytester: Pytester): +def test_parametrized_collect_with_wrong_args(pytester: Pytester) -> None: """Test collect parametrized func with wrong number of args.""" py_file = pytester.makepyfile( """ @@ -419,7 +419,7 @@ def test_func(foo, bar): ) -def test_parametrized_with_kwargs(pytester: Pytester): +def test_parametrized_with_kwargs(pytester: Pytester) -> None: """Test collect parametrized func with wrong number of args.""" py_file = pytester.makepyfile( """ @@ -439,7 +439,7 @@ def test_func(a, b): assert result.ret == 0 -def test_parametrize_iterator(pytester: Pytester): +def test_parametrize_iterator(pytester: Pytester) -> None: """`parametrize` should work with generators (#5354).""" py_file = pytester.makepyfile( """\ @@ -462,7 +462,7 @@ def test(a): class TestFunctional: - def test_merging_markers_deep(self, pytester: Pytester): + def test_merging_markers_deep(self, pytester: Pytester) -> None: # issue 199 - propagate markers into nested classes p = pytester.makepyfile( """ @@ -484,7 +484,7 @@ def test_d(self): def test_mark_decorator_subclass_does_not_propagate_to_base( self, pytester: Pytester - ): + ) -> None: p = pytester.makepyfile( """ import pytest @@ -503,7 +503,7 @@ def test_bar(self): pass items, rec = pytester.inline_genitems(p) self.assert_markers(items, test_foo=("a", "b"), test_bar=("a",)) - def test_mark_should_not_pass_to_siebling_class(self, pytester: Pytester): + def test_mark_should_not_pass_to_siebling_class(self, pytester: Pytester) -> None: """#568""" p = pytester.makepyfile( """ @@ -531,7 +531,7 @@ class TestOtherSub(TestBase): assert not list(sub_item_other.iter_markers(name="b")) assert list(sub_item.iter_markers(name="b")) - def test_mark_decorator_baseclasses_merged(self, pytester: Pytester): + def test_mark_decorator_baseclasses_merged(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ import pytest @@ -554,7 +554,7 @@ def test_bar(self): pass items, rec = pytester.inline_genitems(p) self.assert_markers(items, test_foo=("a", "b", "c"), test_bar=("a", "b", "d")) - def test_mark_closest(self, pytester: Pytester): + def test_mark_closest(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ import pytest @@ -572,11 +572,11 @@ def test_has_inherited(self): ) items, rec = pytester.inline_genitems(p) has_own, has_inherited = items - assert has_own.get_closest_marker("c").kwargs == {"location": "function"} # type: ignore - assert has_inherited.get_closest_marker("c").kwargs == {"location": "class"} # type: ignore + assert has_own.get_closest_marker("c").kwargs == {"location": "function"} # type: ignore[union-attr] + assert has_inherited.get_closest_marker("c").kwargs == {"location": "class"} # type: ignore[union-attr] assert has_own.get_closest_marker("missing") is None - def test_mark_with_wrong_marker(self, pytester: Pytester): + def test_mark_with_wrong_marker(self, pytester: Pytester) -> None: reprec = pytester.inline_runsource( """ import pytest @@ -590,7 +590,7 @@ def test_func(): assert len(values) == 1 assert "TypeError" in str(values[0].longrepr) - def test_mark_dynamically_in_funcarg(self, pytester: Pytester): + def test_mark_dynamically_in_funcarg(self, pytester: Pytester) -> None: pytester.makeconftest( """ import pytest @@ -611,7 +611,7 @@ def test_func(arg): result = pytester.runpytest() result.stdout.fnmatch_lines(["keyword: *hello*"]) - def test_no_marker_match_on_unmarked_names(self, pytester: Pytester): + def test_no_marker_match_on_unmarked_names(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ import pytest @@ -630,7 +630,7 @@ def test_unmarked(): deselected_tests = dlist[0].items assert len(deselected_tests) == 2 - def test_keywords_at_node_level(self, pytester: Pytester): + def test_keywords_at_node_level(self, pytester: Pytester) -> None: pytester.makepyfile( """ import pytest @@ -652,7 +652,7 @@ def test_function(): reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_keyword_added_for_session(self, pytester: Pytester): + def test_keyword_added_for_session(self, pytester: Pytester) -> None: pytester.makeconftest( """ import pytest @@ -680,7 +680,7 @@ def test_some(request): reprec = pytester.inline_run("-m", "mark1") reprec.assertoutcome(passed=1) - def assert_markers(self, items, **expected): + def assert_markers(self, items, **expected) -> None: """Assert that given items have expected marker names applied to them. expected should be a dict of (item name -> seq of expected marker names). @@ -693,7 +693,7 @@ def assert_markers(self, items, **expected): assert markers == set(expected_markers) @pytest.mark.filterwarnings("ignore") - def test_mark_from_parameters(self, pytester: Pytester): + def test_mark_from_parameters(self, pytester: Pytester) -> None: """#1540""" pytester.makepyfile( """ @@ -717,7 +717,7 @@ def test_1(parameter): reprec = pytester.inline_run() reprec.assertoutcome(skipped=1) - def test_reevaluate_dynamic_expr(self, pytester: Pytester): + def test_reevaluate_dynamic_expr(self, pytester: Pytester) -> None: """#7360""" py_file1 = pytester.makepyfile( test_reevaluate_dynamic_expr1=""" @@ -749,7 +749,7 @@ def test_should_not_skip(): class TestKeywordSelection: - def test_select_simple(self, pytester: Pytester): + def test_select_simple(self, pytester: Pytester) -> None: file_test = pytester.makepyfile( """ def test_one(): @@ -782,7 +782,7 @@ def check(keyword, name): "xxx and TestClass and test_2", ], ) - def test_select_extra_keywords(self, pytester: Pytester, keyword): + def test_select_extra_keywords(self, pytester: Pytester, keyword) -> None: p = pytester.makepyfile( test_select=""" def test_1(): @@ -812,7 +812,7 @@ def pytest_pycollect_makeitem(name): assert len(dlist) == 1 assert dlist[0].items[0].name == "test_1" - def test_select_starton(self, pytester: Pytester): + def test_select_starton(self, pytester: Pytester) -> None: threepass = pytester.makepyfile( test_threepass=""" def test_one(): assert 1 @@ -829,7 +829,7 @@ def test_three(): assert 1 item = dlist[0].items[0] assert item.name == "test_one" - def test_keyword_extra(self, pytester: Pytester): + def test_keyword_extra(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ def test_one(): @@ -842,7 +842,7 @@ def test_one(): assert failed == 1 @pytest.mark.xfail - def test_keyword_extra_dash(self, pytester: Pytester): + def test_keyword_extra_dash(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ def test_one(): @@ -876,7 +876,7 @@ def test_one(): assert 1 deselected_tests = dlist[0].items assert len(deselected_tests) == 1 - def test_no_match_directories_outside_the_suite(self, pytester: Pytester): + def test_no_match_directories_outside_the_suite(self, pytester: Pytester) -> None: """`-k` should not match against directories containing the test suite (#7040).""" test_contents = """ def test_aaa(): pass @@ -915,7 +915,7 @@ class TestMarkDecorator: ("foo", pytest.mark.bar(), False), ], ) - def test__eq__(self, lhs, rhs, expected): + def test__eq__(self, lhs, rhs, expected) -> None: assert (lhs == rhs) == expected def test_aliases(self) -> None: @@ -926,7 +926,9 @@ def test_aliases(self) -> None: @pytest.mark.parametrize("mark", [None, "", "skip", "xfail"]) -def test_parameterset_for_parametrize_marks(pytester: Pytester, mark): +def test_parameterset_for_parametrize_marks( + pytester: Pytester, mark: Optional[str] +) -> None: if mark is not None: pytester.makeini( """ @@ -951,7 +953,7 @@ def test_parameterset_for_parametrize_marks(pytester: Pytester, mark): assert result_mark.kwargs.get("run") is False -def test_parameterset_for_fail_at_collect(pytester: Pytester): +def test_parameterset_for_fail_at_collect(pytester: Pytester) -> None: pytester.makeini( """ [pytest] @@ -993,12 +995,12 @@ def test(): assert result.ret == ExitCode.INTERRUPTED -def test_parameterset_for_parametrize_bad_markname(pytester: Pytester): +def test_parameterset_for_parametrize_bad_markname(pytester: Pytester) -> None: with pytest.raises(pytest.UsageError): test_parameterset_for_parametrize_marks(pytester, "bad") -def test_mark_expressions_no_smear(pytester: Pytester): +def test_mark_expressions_no_smear(pytester: Pytester) -> None: pytester.makepyfile( """ import pytest @@ -1033,7 +1035,7 @@ class TestBarClass(BaseTests): # assert skipped_k == failed_k == 0 -def test_addmarker_order(): +def test_addmarker_order() -> None: session = mock.Mock() session.own_markers = [] session.parent = None @@ -1047,7 +1049,7 @@ def test_addmarker_order(): @pytest.mark.filterwarnings("ignore") -def test_markers_from_parametrize(pytester: Pytester): +def test_markers_from_parametrize(pytester: Pytester) -> None: """#3605""" pytester.makepyfile( """ @@ -1092,12 +1094,12 @@ def test_pytest_param_id_requires_string() -> None: @pytest.mark.parametrize("s", (None, "hello world")) -def test_pytest_param_id_allows_none_or_string(s): +def test_pytest_param_id_allows_none_or_string(s) -> None: assert pytest.param(id=s) @pytest.mark.parametrize("expr", ("NOT internal_err", "NOT (internal_err)", "bogus/")) -def test_marker_expr_eval_failure_handling(pytester: Pytester, expr): +def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None: foo = pytester.makepyfile( """ import pytest diff --git a/testing/test_warning_types.py b/testing/test_warning_types.py index 46bcee364fc..b25daccc01d 100644 --- a/testing/test_warning_types.py +++ b/testing/test_warning_types.py @@ -13,7 +13,7 @@ if inspect.isclass(w) and issubclass(w, Warning) ], ) -def test_warning_types(warning_class): +def test_warning_types(warning_class) -> None: """Make sure all warnings declared in _pytest.warning_types are displayed as coming from 'pytest' instead of the internal module (#5452). """ @@ -21,7 +21,7 @@ def test_warning_types(warning_class): @pytest.mark.filterwarnings("error::pytest.PytestWarning") -def test_pytest_warnings_repr_integration_test(pytester: Pytester): +def test_pytest_warnings_repr_integration_test(pytester: Pytester) -> None: """Small integration test to ensure our small hack of setting the __module__ attribute of our warnings actually works (#5452). """ From cde50db6e7ad263814b9542c9cb362d6cb7d7222 Mon Sep 17 00:00:00 2001 From: symonk Date: Sun, 25 Oct 2020 18:31:43 +0000 Subject: [PATCH 725/823] add type hint to parametrized warning_class --- testing/test_warning_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/test_warning_types.py b/testing/test_warning_types.py index b25daccc01d..b49cc68f9c6 100644 --- a/testing/test_warning_types.py +++ b/testing/test_warning_types.py @@ -1,7 +1,7 @@ import inspect -import _pytest.warning_types import pytest +from _pytest import warning_types from _pytest.pytester import Pytester @@ -9,11 +9,11 @@ "warning_class", [ w - for n, w in vars(_pytest.warning_types).items() + for n, w in vars(warning_types).items() if inspect.isclass(w) and issubclass(w, Warning) ], ) -def test_warning_types(warning_class) -> None: +def test_warning_types(warning_class: UserWarning) -> None: """Make sure all warnings declared in _pytest.warning_types are displayed as coming from 'pytest' instead of the internal module (#5452). """ From f7c50678231c58231eb94259e6b807c2740ea2fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 03:15:50 +0000 Subject: [PATCH 726/823] build(deps): bump pytest-django in /testing/plugins_integration Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/pytest-dev/pytest-django/releases) - [Changelog](https://github.com/pytest-dev/pytest-django/blob/master/docs/changelog.rst) - [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.0.0...v4.1.0) 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 7c7dfe36355..cdd55a0e51d 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -3,7 +3,7 @@ django==3.1.2 pytest-asyncio==0.14.0 pytest-bdd==4.0.1 pytest-cov==2.10.1 -pytest-django==4.0.0 +pytest-django==4.1.0 pytest-flakes==4.0.2 pytest-html==2.1.1 pytest-mock==3.3.1 From c31f4dc112231d0b061561541360f784adf29b03 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 26 Oct 2020 15:01:38 +0200 Subject: [PATCH 727/823] testing: make conftest stuff check for pytester not testdir testdir uses pytester so this applies to it as well, but now includes pytester as well. --- testing/conftest.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/testing/conftest.py b/testing/conftest.py index 62a2b4a22b8..2dc20bcb2fd 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -3,7 +3,8 @@ from typing import List import pytest -from _pytest.pytester import Testdir +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Pytester if sys.gettrace(): @@ -41,7 +42,7 @@ def pytest_collection_modifyitems(items): # (https://github.com/pytest-dev/pytest/issues/5070) neutral_items.append(item) else: - if "testdir" in fixtures: + if "pytester" in fixtures: co_names = item.function.__code__.co_names if spawn_names.intersection(co_names): item.add_marker(pytest.mark.uses_pexpect) @@ -103,13 +104,13 @@ def get_write_msg(self, idx): @pytest.fixture -def dummy_yaml_custom_test(testdir): +def dummy_yaml_custom_test(pytester: Pytester): """Writes a conftest file that collects and executes a dummy yaml test. Taken from the docs, but stripped down to the bare minimum, useful for tests which needs custom items collected. """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -126,13 +127,13 @@ def runtest(self): pass """ ) - testdir.makefile(".yaml", test1="") + pytester.makefile(".yaml", test1="") @pytest.fixture -def testdir(testdir: Testdir) -> Testdir: - testdir.monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") - return testdir +def pytester(pytester: Pytester, monkeypatch: MonkeyPatch) -> Pytester: + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") + return pytester @pytest.fixture(scope="session") @@ -178,7 +179,7 @@ def format_for_rematch(cls, lines: List[str]) -> List[str]: @pytest.fixture -def mock_timing(monkeypatch): +def mock_timing(monkeypatch: MonkeyPatch): """Mocks _pytest.timing with a known object that can be used to control timing in tests deterministically. From cd9b3618c77c2c748104b0e7d7dd9e063bc6c54e Mon Sep 17 00:00:00 2001 From: Mikhail Fesenko Date: Mon, 26 Oct 2020 00:45:10 +0300 Subject: [PATCH 728/823] #7942 test_helpconfig.py migrate from testdir to pytester --- testing/test_helpconfig.py | 57 +++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index 6f6d533372e..c2533ef304a 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -1,26 +1,27 @@ import pytest from _pytest.config import ExitCode +from _pytest.pytester import Pytester -def test_version_verbose(testdir, pytestconfig): - testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") - result = testdir.runpytest("--version", "--version") +def test_version_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None: + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + result = pytester.runpytest("--version", "--version") assert result.ret == 0 result.stderr.fnmatch_lines([f"*pytest*{pytest.__version__}*imported from*"]) if pytestconfig.pluginmanager.list_plugin_distinfo(): result.stderr.fnmatch_lines(["*setuptools registered plugins:", "*at*"]) -def test_version_less_verbose(testdir, pytestconfig): - testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") - result = testdir.runpytest("--version") +def test_version_less_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None: + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + result = pytester.runpytest("--version") assert result.ret == 0 # p = py.path.local(py.__file__).dirpath() result.stderr.fnmatch_lines([f"pytest {pytest.__version__}"]) -def test_help(testdir): - result = testdir.runpytest("--help") +def test_help(pytester: Pytester) -> None: + result = pytester.runpytest("--help") assert result.ret == 0 result.stdout.fnmatch_lines( """ @@ -36,29 +37,29 @@ def test_help(testdir): ) -def test_none_help_param_raises_exception(testdir): +def test_none_help_param_raises_exception(pytester: Pytester) -> None: """Test that a None help param raises a TypeError.""" - testdir.makeconftest( + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("test_ini", None, default=True, type="bool") """ ) - result = testdir.runpytest("--help") + result = pytester.runpytest("--help") result.stderr.fnmatch_lines( ["*TypeError: help argument cannot be None for test_ini*"] ) -def test_empty_help_param(testdir): +def test_empty_help_param(pytester: Pytester) -> None: """Test that an empty help param is displayed correctly.""" - testdir.makeconftest( + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("test_ini", "", default=True, type="bool") """ ) - result = testdir.runpytest("--help") + result = pytester.runpytest("--help") assert result.ret == 0 lines = [ " required_plugins (args):", @@ -69,20 +70,20 @@ def pytest_addoption(parser): result.stdout.fnmatch_lines(lines, consecutive=True) -def test_hookvalidation_unknown(testdir): - testdir.makeconftest( +def test_hookvalidation_unknown(pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_hello(xyz): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines(["*unknown hook*pytest_hello*"]) -def test_hookvalidation_optional(testdir): - testdir.makeconftest( +def test_hookvalidation_optional(pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @pytest.hookimpl(optionalhook=True) @@ -90,25 +91,25 @@ def pytest_hello(xyz): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED -def test_traceconfig(testdir): - result = testdir.runpytest("--traceconfig") +def test_traceconfig(pytester: Pytester) -> None: + result = pytester.runpytest("--traceconfig") result.stdout.fnmatch_lines(["*using*pytest*py*", "*active plugins*"]) -def test_debug(testdir): - result = testdir.runpytest_subprocess("--debug") +def test_debug(pytester: Pytester) -> None: + result = pytester.runpytest_subprocess("--debug") assert result.ret == ExitCode.NO_TESTS_COLLECTED - p = testdir.tmpdir.join("pytestdebug.log") - assert "pytest_sessionstart" in p.read() + p = pytester.path.joinpath("pytestdebug.log") + assert "pytest_sessionstart" in p.read_text("utf-8") -def test_PYTEST_DEBUG(testdir, monkeypatch): +def test_PYTEST_DEBUG(pytester: Pytester, monkeypatch) -> None: monkeypatch.setenv("PYTEST_DEBUG", "1") - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stderr.fnmatch_lines( ["*pytest_plugin_registered*", "*manager*PluginManager*"] From b308c6ddb374d57c3db982d2749cfaf5eccd3b1a Mon Sep 17 00:00:00 2001 From: Gustavo Camargo Date: Mon, 26 Oct 2020 14:30:00 -0500 Subject: [PATCH 729/823] Update doctest.rst --- doc/en/doctest.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 5a3c76a126f..c6e34b2b1c8 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -113,7 +113,7 @@ lengthy exception stack traces you can just write: .. code-block:: ini [pytest] - doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL + doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL Alternatively, options can be enabled by an inline comment in the doc test itself: From 434e30424e23ebf963412c2378ed9809792f6b15 Mon Sep 17 00:00:00 2001 From: symonk Date: Tue, 27 Oct 2020 17:50:54 +0000 Subject: [PATCH 730/823] Address feedback for converting testdir to pytester --- testing/test_mark.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/testing/test_mark.py b/testing/test_mark.py index bc538fe5db2..e0b91f0cef4 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -184,7 +184,9 @@ def test_hello(): @pytest.mark.parametrize("option_name", ["--strict-markers", "--strict"]) -def test_strict_prohibits_unregistered_markers(pytester: Pytester, option_name) -> None: +def test_strict_prohibits_unregistered_markers( + pytester: Pytester, option_name: str +) -> None: pytester.makepyfile( """ import pytest @@ -572,8 +574,12 @@ def test_has_inherited(self): ) items, rec = pytester.inline_genitems(p) has_own, has_inherited = items - assert has_own.get_closest_marker("c").kwargs == {"location": "function"} # type: ignore[union-attr] - assert has_inherited.get_closest_marker("c").kwargs == {"location": "class"} # type: ignore[union-attr] + has_own_marker = has_own.get_closest_marker("c") + has_inherited_marker = has_inherited.get_closest_marker("c") + assert has_own_marker is not None + assert has_inherited_marker is not None + assert has_own_marker.kwargs == {"location": "function"} + assert has_inherited_marker.kwargs == {"location": "class"} assert has_own.get_closest_marker("missing") is None def test_mark_with_wrong_marker(self, pytester: Pytester) -> None: From a431310c0a36b83907c87087c84c246fbffdd2fa Mon Sep 17 00:00:00 2001 From: Vasilis Gerakaris Date: Wed, 28 Oct 2020 13:23:35 +0200 Subject: [PATCH 731/823] Increase temp dir deletion period to 3 days (#7914) Co-authored-by: Bruno Oliveira --- changelog/7911.bugfix.rst | 1 + src/_pytest/pathlib.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/7911.bugfix.rst diff --git a/changelog/7911.bugfix.rst b/changelog/7911.bugfix.rst new file mode 100644 index 00000000000..5b85b20b5bc --- /dev/null +++ b/changelog/7911.bugfix.rst @@ -0,0 +1 @@ +Directories created by `tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 0bc5bff2bb5..f0bdb1481bb 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -32,7 +32,7 @@ from _pytest.outcomes import skip from _pytest.warning_types import PytestWarning -LOCK_TIMEOUT = 60 * 60 * 3 +LOCK_TIMEOUT = 60 * 60 * 24 * 3 _AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath) From 8d369f73bab0e490b6bb3749e5cb7f3d405e52ab Mon Sep 17 00:00:00 2001 From: Christine M Date: Wed, 28 Oct 2020 10:05:54 -0500 Subject: [PATCH 732/823] Migrate test_skipping.py from testdir to pytester (#7953) --- AUTHORS | 1 + testing/test_skipping.py | 416 ++++++++++++++++++++------------------- 2 files changed, 214 insertions(+), 203 deletions(-) diff --git a/AUTHORS b/AUTHORS index d2bb3d3ec0f..35d220e0044 100644 --- a/AUTHORS +++ b/AUTHORS @@ -60,6 +60,7 @@ Christian Fetzer Christian Neumüller Christian Theunert Christian Tismer +Christine Mecklenborg Christoph Buelter Christopher Dignam Christopher Gilling diff --git a/testing/test_skipping.py b/testing/test_skipping.py index bf014e343c0..cfc0cdbca5e 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1,7 +1,7 @@ import sys import pytest -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester from _pytest.runner import runtestprotocol from _pytest.skipping import evaluate_skip_marks from _pytest.skipping import evaluate_xfail_marks @@ -9,13 +9,13 @@ class TestEvaluation: - def test_no_marker(self, testdir): - item = testdir.getitem("def test_func(): pass") + def test_no_marker(self, pytester: Pytester) -> None: + item = pytester.getitem("def test_func(): pass") skipped = evaluate_skip_marks(item) assert not skipped - def test_marked_xfail_no_args(self, testdir): - item = testdir.getitem( + def test_marked_xfail_no_args(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.xfail @@ -28,8 +28,8 @@ def test_func(): assert xfailed.reason == "" assert xfailed.run - def test_marked_skipif_no_args(self, testdir): - item = testdir.getitem( + def test_marked_skipif_no_args(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.skipif @@ -41,8 +41,8 @@ def test_func(): assert skipped assert skipped.reason == "" - def test_marked_one_arg(self, testdir): - item = testdir.getitem( + def test_marked_one_arg(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.skipif("hasattr(os, 'sep')") @@ -54,8 +54,8 @@ def test_func(): assert skipped assert skipped.reason == "condition: hasattr(os, 'sep')" - def test_marked_one_arg_with_reason(self, testdir): - item = testdir.getitem( + def test_marked_one_arg_with_reason(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.skipif("hasattr(os, 'sep')", attr=2, reason="hello world") @@ -67,13 +67,13 @@ def test_func(): assert skipped assert skipped.reason == "hello world" - def test_marked_one_arg_twice(self, testdir): + def test_marked_one_arg_twice(self, pytester: Pytester) -> None: lines = [ """@pytest.mark.skipif("not hasattr(os, 'murks')")""", """@pytest.mark.skipif(condition="hasattr(os, 'murks')")""", ] for i in range(0, 2): - item = testdir.getitem( + item = pytester.getitem( """ import pytest %s @@ -87,8 +87,8 @@ def test_func(): assert skipped assert skipped.reason == "condition: not hasattr(os, 'murks')" - def test_marked_one_arg_twice2(self, testdir): - item = testdir.getitem( + def test_marked_one_arg_twice2(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.skipif("hasattr(os, 'murks')") @@ -101,8 +101,10 @@ def test_func(): assert skipped assert skipped.reason == "condition: not hasattr(os, 'murks')" - def test_marked_skipif_with_boolean_without_reason(self, testdir) -> None: - item = testdir.getitem( + def test_marked_skipif_with_boolean_without_reason( + self, pytester: Pytester + ) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.skipif(False) @@ -118,8 +120,8 @@ def test_func(): in excinfo.value.msg ) - def test_marked_skipif_with_invalid_boolean(self, testdir) -> None: - item = testdir.getitem( + def test_marked_skipif_with_invalid_boolean(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @@ -138,8 +140,8 @@ def test_func(): assert "Error evaluating 'skipif' condition as a boolean" in excinfo.value.msg assert "INVALID" in excinfo.value.msg - def test_skipif_class(self, testdir): - (item,) = testdir.getitems( + def test_skipif_class(self, pytester: Pytester) -> None: + (item,) = pytester.getitems( """ import pytest class TestClass(object): @@ -148,7 +150,7 @@ def test_func(self): pass """ ) - item.config._hackxyz = 3 + item.config._hackxyz = 3 # type: ignore[attr-defined] skipped = evaluate_skip_marks(item) assert skipped assert skipped.reason == "condition: config._hackxyz" @@ -156,8 +158,8 @@ def test_func(self): class TestXFail: @pytest.mark.parametrize("strict", [True, False]) - def test_xfail_simple(self, testdir, strict): - item = testdir.getitem( + def test_xfail_simple(self, pytester: Pytester, strict: bool) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.xfail(strict=%s) @@ -172,8 +174,8 @@ def test_func(): assert callreport.skipped assert callreport.wasxfail == "" - def test_xfail_xpassed(self, testdir): - item = testdir.getitem( + def test_xfail_xpassed(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.xfail(reason="this is an xfail") @@ -187,9 +189,9 @@ def test_func(): assert callreport.passed assert callreport.wasxfail == "this is an xfail" - def test_xfail_using_platform(self, testdir): + def test_xfail_using_platform(self, pytester: Pytester) -> None: """Verify that platform can be used with xfail statements.""" - item = testdir.getitem( + item = pytester.getitem( """ import pytest @pytest.mark.xfail("platform.platform() == platform.platform()") @@ -202,8 +204,8 @@ def test_func(): callreport = reports[1] assert callreport.wasxfail - def test_xfail_xpassed_strict(self, testdir): - item = testdir.getitem( + def test_xfail_xpassed_strict(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.xfail(strict=True, reason="nope") @@ -218,8 +220,8 @@ def test_func(): assert str(callreport.longrepr) == "[XPASS(strict)] nope" assert not hasattr(callreport, "wasxfail") - def test_xfail_run_anyway(self, testdir): - testdir.makepyfile( + def test_xfail_run_anyway(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.xfail @@ -229,7 +231,7 @@ def test_func2(): pytest.xfail("hello") """ ) - result = testdir.runpytest("--runxfail") + result = pytester.runpytest("--runxfail") result.stdout.fnmatch_lines( ["*def test_func():*", "*assert 0*", "*1 failed*1 pass*"] ) @@ -247,8 +249,10 @@ def test_func2(): ), ], ) - def test_xfail_run_with_skip_mark(self, testdir, test_input, expected): - testdir.makepyfile( + def test_xfail_run_with_skip_mark( + self, pytester: Pytester, test_input, expected + ) -> None: + pytester.makepyfile( test_sample=""" import pytest @pytest.mark.skip @@ -256,11 +260,11 @@ def test_skip_location() -> None: assert 0 """ ) - result = testdir.runpytest(*test_input) + result = pytester.runpytest(*test_input) result.stdout.fnmatch_lines(expected) - def test_xfail_evalfalse_but_fails(self, testdir): - item = testdir.getitem( + def test_xfail_evalfalse_but_fails(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.xfail('False') @@ -274,8 +278,8 @@ def test_func(): assert not hasattr(callreport, "wasxfail") assert "xfail" in callreport.keywords - def test_xfail_not_report_default(self, testdir): - p = testdir.makepyfile( + def test_xfail_not_report_default(self, pytester: Pytester) -> None: + p = pytester.makepyfile( test_one=""" import pytest @pytest.mark.xfail @@ -283,13 +287,13 @@ def test_this(): assert 0 """ ) - testdir.runpytest(p, "-v") + pytester.runpytest(p, "-v") # result.stdout.fnmatch_lines([ # "*HINT*use*-r*" # ]) - def test_xfail_not_run_xfail_reporting(self, testdir): - p = testdir.makepyfile( + def test_xfail_not_run_xfail_reporting(self, pytester: Pytester) -> None: + p = pytester.makepyfile( test_one=""" import pytest @pytest.mark.xfail(run=False, reason="noway") @@ -303,7 +307,7 @@ def test_this_false(): assert 1 """ ) - result = testdir.runpytest(p, "-rx") + result = pytester.runpytest(p, "-rx") result.stdout.fnmatch_lines( [ "*test_one*test_this*", @@ -314,8 +318,8 @@ def test_this_false(): ] ) - def test_xfail_not_run_no_setup_run(self, testdir): - p = testdir.makepyfile( + def test_xfail_not_run_no_setup_run(self, pytester: Pytester) -> None: + p = pytester.makepyfile( test_one=""" import pytest @pytest.mark.xfail(run=False, reason="hello") @@ -325,13 +329,13 @@ def setup_module(mod): raise ValueError(42) """ ) - result = testdir.runpytest(p, "-rx") + result = pytester.runpytest(p, "-rx") result.stdout.fnmatch_lines( ["*test_one*test_this*", "*NOTRUN*hello", "*1 xfailed*"] ) - def test_xfail_xpass(self, testdir): - p = testdir.makepyfile( + def test_xfail_xpass(self, pytester: Pytester) -> None: + p = pytester.makepyfile( test_one=""" import pytest @pytest.mark.xfail @@ -339,27 +343,27 @@ def test_that(): assert 1 """ ) - result = testdir.runpytest(p, "-rX") + result = pytester.runpytest(p, "-rX") result.stdout.fnmatch_lines(["*XPASS*test_that*", "*1 xpassed*"]) assert result.ret == 0 - def test_xfail_imperative(self, testdir): - p = testdir.makepyfile( + def test_xfail_imperative(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest def test_this(): pytest.xfail("hello") """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*1 xfailed*"]) - result = testdir.runpytest(p, "-rx") + result = pytester.runpytest(p, "-rx") result.stdout.fnmatch_lines(["*XFAIL*test_this*", "*reason:*hello*"]) - result = testdir.runpytest(p, "--runxfail") + result = pytester.runpytest(p, "--runxfail") result.stdout.fnmatch_lines(["*1 pass*"]) - def test_xfail_imperative_in_setup_function(self, testdir): - p = testdir.makepyfile( + def test_xfail_imperative_in_setup_function(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest def setup_function(function): @@ -369,11 +373,11 @@ def test_this(): assert 0 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*1 xfailed*"]) - result = testdir.runpytest(p, "-rx") + result = pytester.runpytest(p, "-rx") result.stdout.fnmatch_lines(["*XFAIL*test_this*", "*reason:*hello*"]) - result = testdir.runpytest(p, "--runxfail") + result = pytester.runpytest(p, "--runxfail") result.stdout.fnmatch_lines( """ *def test_this* @@ -381,8 +385,8 @@ def test_this(): """ ) - def xtest_dynamic_xfail_set_during_setup(self, testdir): - p = testdir.makepyfile( + def xtest_dynamic_xfail_set_during_setup(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest def setup_function(function): @@ -393,11 +397,11 @@ def test_that(): assert 1 """ ) - result = testdir.runpytest(p, "-rxX") + result = pytester.runpytest(p, "-rxX") result.stdout.fnmatch_lines(["*XFAIL*test_this*", "*XPASS*test_that*"]) - def test_dynamic_xfail_no_run(self, testdir): - p = testdir.makepyfile( + def test_dynamic_xfail_no_run(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest @pytest.fixture @@ -407,11 +411,11 @@ def test_this(arg): assert 0 """ ) - result = testdir.runpytest(p, "-rxX") + result = pytester.runpytest(p, "-rxX") result.stdout.fnmatch_lines(["*XFAIL*test_this*", "*NOTRUN*"]) - def test_dynamic_xfail_set_during_funcarg_setup(self, testdir): - p = testdir.makepyfile( + def test_dynamic_xfail_set_during_funcarg_setup(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest @pytest.fixture @@ -421,12 +425,12 @@ def test_this2(arg): assert 0 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*1 xfailed*"]) - def test_dynamic_xfail_set_during_runtest_failed(self, testdir: Testdir) -> None: + def test_dynamic_xfail_set_during_runtest_failed(self, pytester: Pytester) -> None: # Issue #7486. - p = testdir.makepyfile( + p = pytester.makepyfile( """ import pytest def test_this(request): @@ -434,21 +438,21 @@ def test_this(request): assert 0 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.assert_outcomes(xfailed=1) def test_dynamic_xfail_set_during_runtest_passed_strict( - self, testdir: Testdir + self, pytester: Pytester ) -> None: # Issue #7486. - p = testdir.makepyfile( + p = pytester.makepyfile( """ import pytest def test_this(request): request.node.add_marker(pytest.mark.xfail(reason="xfail", strict=True)) """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.assert_outcomes(failed=1) @pytest.mark.parametrize( @@ -460,8 +464,10 @@ def test_this(request): ("(AttributeError, TypeError)", "IndexError", "*1 failed*"), ], ) - def test_xfail_raises(self, expected, actual, matchline, testdir): - p = testdir.makepyfile( + def test_xfail_raises( + self, expected, actual, matchline, pytester: Pytester + ) -> None: + p = pytester.makepyfile( """ import pytest @pytest.mark.xfail(raises=%s) @@ -470,13 +476,13 @@ def test_raises(): """ % (expected, actual) ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines([matchline]) - def test_strict_sanity(self, testdir): + def test_strict_sanity(self, pytester: Pytester) -> None: """Sanity check for xfail(strict=True): a failing test should behave exactly like a normal xfail.""" - p = testdir.makepyfile( + p = pytester.makepyfile( """ import pytest @pytest.mark.xfail(reason='unsupported feature', strict=True) @@ -484,13 +490,13 @@ def test_foo(): assert 0 """ ) - result = testdir.runpytest(p, "-rxX") + result = pytester.runpytest(p, "-rxX") result.stdout.fnmatch_lines(["*XFAIL*", "*unsupported feature*"]) assert result.ret == 0 @pytest.mark.parametrize("strict", [True, False]) - def test_strict_xfail(self, testdir, strict): - p = testdir.makepyfile( + def test_strict_xfail(self, pytester: Pytester, strict: bool) -> None: + p = pytester.makepyfile( """ import pytest @@ -500,7 +506,7 @@ def test_foo(): """ % strict ) - result = testdir.runpytest(p, "-rxX") + result = pytester.runpytest(p, "-rxX") if strict: result.stdout.fnmatch_lines( ["*test_foo*", "*XPASS(strict)*unsupported feature*"] @@ -513,11 +519,11 @@ def test_foo(): ] ) assert result.ret == (1 if strict else 0) - assert testdir.tmpdir.join("foo_executed").isfile() + assert pytester.path.joinpath("foo_executed").exists() @pytest.mark.parametrize("strict", [True, False]) - def test_strict_xfail_condition(self, testdir, strict): - p = testdir.makepyfile( + def test_strict_xfail_condition(self, pytester: Pytester, strict: bool) -> None: + p = pytester.makepyfile( """ import pytest @@ -527,13 +533,13 @@ def test_foo(): """ % strict ) - result = testdir.runpytest(p, "-rxX") + result = pytester.runpytest(p, "-rxX") result.stdout.fnmatch_lines(["*1 passed*"]) assert result.ret == 0 @pytest.mark.parametrize("strict", [True, False]) - def test_xfail_condition_keyword(self, testdir, strict): - p = testdir.makepyfile( + def test_xfail_condition_keyword(self, pytester: Pytester, strict: bool) -> None: + p = pytester.makepyfile( """ import pytest @@ -543,20 +549,22 @@ def test_foo(): """ % strict ) - result = testdir.runpytest(p, "-rxX") + result = pytester.runpytest(p, "-rxX") result.stdout.fnmatch_lines(["*1 passed*"]) assert result.ret == 0 @pytest.mark.parametrize("strict_val", ["true", "false"]) - def test_strict_xfail_default_from_file(self, testdir, strict_val): - testdir.makeini( + def test_strict_xfail_default_from_file( + self, pytester: Pytester, strict_val + ) -> None: + pytester.makeini( """ [pytest] xfail_strict = %s """ % strict_val ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ import pytest @pytest.mark.xfail(reason='unsupported feature') @@ -564,15 +572,15 @@ def test_foo(): pass """ ) - result = testdir.runpytest(p, "-rxX") + result = pytester.runpytest(p, "-rxX") strict = strict_val == "true" result.stdout.fnmatch_lines(["*1 failed*" if strict else "*1 xpassed*"]) assert result.ret == (1 if strict else 0) class TestXFailwithSetupTeardown: - def test_failing_setup_issue9(self, testdir): - testdir.makepyfile( + def test_failing_setup_issue9(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest def setup_function(func): @@ -583,11 +591,11 @@ def test_func(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 xfail*"]) - def test_failing_teardown_issue9(self, testdir): - testdir.makepyfile( + def test_failing_teardown_issue9(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest def teardown_function(func): @@ -598,13 +606,13 @@ def test_func(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 xfail*"]) class TestSkip: - def test_skip_class(self, testdir): - testdir.makepyfile( + def test_skip_class(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skip @@ -618,11 +626,11 @@ def test_baz(): pass """ ) - rec = testdir.inline_run() + rec = pytester.inline_run() rec.assertoutcome(skipped=2, passed=1) - def test_skips_on_false_string(self, testdir): - testdir.makepyfile( + def test_skips_on_false_string(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skip('False') @@ -630,11 +638,11 @@ def test_foo(): pass """ ) - rec = testdir.inline_run() + rec = pytester.inline_run() rec.assertoutcome(skipped=1) - def test_arg_as_reason(self, testdir): - testdir.makepyfile( + def test_arg_as_reason(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skip('testing stuff') @@ -642,11 +650,11 @@ def test_bar(): pass """ ) - result = testdir.runpytest("-rs") + result = pytester.runpytest("-rs") result.stdout.fnmatch_lines(["*testing stuff*", "*1 skipped*"]) - def test_skip_no_reason(self, testdir): - testdir.makepyfile( + def test_skip_no_reason(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skip @@ -654,11 +662,11 @@ def test_foo(): pass """ ) - result = testdir.runpytest("-rs") + result = pytester.runpytest("-rs") result.stdout.fnmatch_lines(["*unconditional skip*", "*1 skipped*"]) - def test_skip_with_reason(self, testdir): - testdir.makepyfile( + def test_skip_with_reason(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skip(reason="for lolz") @@ -666,11 +674,11 @@ def test_bar(): pass """ ) - result = testdir.runpytest("-rs") + result = pytester.runpytest("-rs") result.stdout.fnmatch_lines(["*for lolz*", "*1 skipped*"]) - def test_only_skips_marked_test(self, testdir): - testdir.makepyfile( + def test_only_skips_marked_test(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skip @@ -683,11 +691,11 @@ def test_baz(): assert True """ ) - result = testdir.runpytest("-rs") + result = pytester.runpytest("-rs") result.stdout.fnmatch_lines(["*nothing in particular*", "*1 passed*2 skipped*"]) - def test_strict_and_skip(self, testdir): - testdir.makepyfile( + def test_strict_and_skip(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skip @@ -695,13 +703,13 @@ def test_hello(): pass """ ) - result = testdir.runpytest("-rs") + result = pytester.runpytest("-rs") result.stdout.fnmatch_lines(["*unconditional skip*", "*1 skipped*"]) class TestSkipif: - def test_skipif_conditional(self, testdir): - item = testdir.getitem( + def test_skipif_conditional(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.skipif("hasattr(os, 'sep')") @@ -715,8 +723,8 @@ def test_func(): @pytest.mark.parametrize( "params", ["\"hasattr(sys, 'platform')\"", 'True, reason="invalid platform"'] ) - def test_skipif_reporting(self, testdir, params): - p = testdir.makepyfile( + def test_skipif_reporting(self, pytester: Pytester, params) -> None: + p = pytester.makepyfile( test_foo=""" import pytest @pytest.mark.skipif(%(params)s) @@ -725,12 +733,12 @@ def test_that(): """ % dict(params=params) ) - result = testdir.runpytest(p, "-s", "-rs") + result = pytester.runpytest(p, "-s", "-rs") result.stdout.fnmatch_lines(["*SKIP*1*test_foo.py*platform*", "*1 skipped*"]) assert result.ret == 0 - def test_skipif_using_platform(self, testdir): - item = testdir.getitem( + def test_skipif_using_platform(self, pytester: Pytester) -> None: + item = pytester.getitem( """ import pytest @pytest.mark.skipif("platform.platform() == platform.platform()") @@ -744,8 +752,10 @@ def test_func(): "marker, msg1, msg2", [("skipif", "SKIP", "skipped"), ("xfail", "XPASS", "xpassed")], ) - def test_skipif_reporting_multiple(self, testdir, marker, msg1, msg2): - testdir.makepyfile( + def test_skipif_reporting_multiple( + self, pytester: Pytester, marker, msg1, msg2 + ) -> None: + pytester.makepyfile( test_foo=""" import pytest @pytest.mark.{marker}(False, reason='first_condition') @@ -756,22 +766,22 @@ def test_foobar(): marker=marker ) ) - result = testdir.runpytest("-s", "-rsxX") + result = pytester.runpytest("-s", "-rsxX") result.stdout.fnmatch_lines( [f"*{msg1}*test_foo.py*second_condition*", f"*1 {msg2}*"] ) assert result.ret == 0 -def test_skip_not_report_default(testdir): - p = testdir.makepyfile( +def test_skip_not_report_default(pytester: Pytester) -> None: + p = pytester.makepyfile( test_one=""" import pytest def test_this(): pytest.skip("hello") """ ) - result = testdir.runpytest(p, "-v") + result = pytester.runpytest(p, "-v") result.stdout.fnmatch_lines( [ # "*HINT*use*-r*", @@ -780,8 +790,8 @@ def test_this(): ) -def test_skipif_class(testdir): - p = testdir.makepyfile( +def test_skipif_class(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest @@ -793,12 +803,12 @@ def test_though(self): assert 0 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*2 skipped*"]) -def test_skipped_reasons_functional(testdir): - testdir.makepyfile( +def test_skipped_reasons_functional(pytester: Pytester) -> None: + pytester.makepyfile( test_one=""" import pytest from conftest import doskip @@ -824,7 +834,7 @@ def doskip(): pytest.skip('test') """, ) - result = testdir.runpytest("-rs") + result = pytester.runpytest("-rs") result.stdout.fnmatch_lines_random( [ "SKIPPED [[]2[]] conftest.py:4: test", @@ -834,8 +844,8 @@ def doskip(): assert result.ret == 0 -def test_skipped_folding(testdir): - testdir.makepyfile( +def test_skipped_folding(pytester: Pytester) -> None: + pytester.makepyfile( test_one=""" import pytest pytestmark = pytest.mark.skip("Folding") @@ -848,13 +858,13 @@ def test_method(self): pass """ ) - result = testdir.runpytest("-rs") + result = pytester.runpytest("-rs") result.stdout.fnmatch_lines(["*SKIP*2*test_one.py: Folding"]) assert result.ret == 0 -def test_reportchars(testdir): - testdir.makepyfile( +def test_reportchars(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest def test_1(): @@ -869,14 +879,14 @@ def test_4(): pytest.skip("four") """ ) - result = testdir.runpytest("-rfxXs") + result = pytester.runpytest("-rfxXs") result.stdout.fnmatch_lines( ["FAIL*test_1*", "XFAIL*test_2*", "XPASS*test_3*", "SKIP*four*"] ) -def test_reportchars_error(testdir): - testdir.makepyfile( +def test_reportchars_error(pytester: Pytester) -> None: + pytester.makepyfile( conftest=""" def pytest_runtest_teardown(): assert 0 @@ -886,12 +896,12 @@ def test_foo(): pass """, ) - result = testdir.runpytest("-rE") + result = pytester.runpytest("-rE") result.stdout.fnmatch_lines(["ERROR*test_foo*"]) -def test_reportchars_all(testdir): - testdir.makepyfile( +def test_reportchars_all(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest def test_1(): @@ -911,7 +921,7 @@ def test_5(fail): pass """ ) - result = testdir.runpytest("-ra") + result = pytester.runpytest("-ra") result.stdout.fnmatch_lines( [ "SKIP*four*", @@ -923,8 +933,8 @@ def test_5(fail): ) -def test_reportchars_all_error(testdir): - testdir.makepyfile( +def test_reportchars_all_error(pytester: Pytester) -> None: + pytester.makepyfile( conftest=""" def pytest_runtest_teardown(): assert 0 @@ -934,12 +944,12 @@ def test_foo(): pass """, ) - result = testdir.runpytest("-ra") + result = pytester.runpytest("-ra") result.stdout.fnmatch_lines(["ERROR*test_foo*"]) -def test_errors_in_xfail_skip_expressions(testdir) -> None: - testdir.makepyfile( +def test_errors_in_xfail_skip_expressions(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skipif("asd") @@ -953,7 +963,7 @@ def test_func(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() markline = " ^" pypy_version_info = getattr(sys, "pypy_version_info", None) if pypy_version_info is not None and pypy_version_info < (6,): @@ -975,8 +985,8 @@ def test_func(): ) -def test_xfail_skipif_with_globals(testdir): - testdir.makepyfile( +def test_xfail_skipif_with_globals(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest x = 3 @@ -988,12 +998,12 @@ def test_boolean(): assert 0 """ ) - result = testdir.runpytest("-rsx") + result = pytester.runpytest("-rsx") result.stdout.fnmatch_lines(["*SKIP*x == 3*", "*XFAIL*test_boolean*", "*x == 3*"]) -def test_default_markers(testdir): - result = testdir.runpytest("--markers") +def test_default_markers(pytester: Pytester) -> None: + result = pytester.runpytest("--markers") result.stdout.fnmatch_lines( [ "*skipif(condition, ..., [*], reason=...)*skip*", @@ -1002,14 +1012,14 @@ def test_default_markers(testdir): ) -def test_xfail_test_setup_exception(testdir): - testdir.makeconftest( +def test_xfail_test_setup_exception(pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_runtest_setup(): 0 / 0 """ ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ import pytest @pytest.mark.xfail @@ -1017,14 +1027,14 @@ def test_func(): assert 0 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) assert result.ret == 0 assert "xfailed" in result.stdout.str() result.stdout.no_fnmatch_line("*xpassed*") -def test_imperativeskip_on_xfail_test(testdir): - testdir.makepyfile( +def test_imperativeskip_on_xfail_test(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.xfail @@ -1036,14 +1046,14 @@ def test_hello(): pass """ ) - testdir.makeconftest( + pytester.makeconftest( """ import pytest def pytest_runtest_setup(item): pytest.skip("abc") """ ) - result = testdir.runpytest("-rsxX") + result = pytester.runpytest("-rsxX") result.stdout.fnmatch_lines_random( """ *SKIP*abc* @@ -1054,8 +1064,8 @@ def pytest_runtest_setup(item): class TestBooleanCondition: - def test_skipif(self, testdir): - testdir.makepyfile( + def test_skipif(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skipif(True, reason="True123") @@ -1066,15 +1076,15 @@ def test_func2(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( """ *1 passed*1 skipped* """ ) - def test_skipif_noreason(self, testdir): - testdir.makepyfile( + def test_skipif_noreason(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.skipif(True) @@ -1082,15 +1092,15 @@ def test_func(): pass """ ) - result = testdir.runpytest("-rs") + result = pytester.runpytest("-rs") result.stdout.fnmatch_lines( """ *1 error* """ ) - def test_xfail(self, testdir): - testdir.makepyfile( + def test_xfail(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.xfail(True, reason="True123") @@ -1098,7 +1108,7 @@ def test_func(): assert 0 """ ) - result = testdir.runpytest("-rxs") + result = pytester.runpytest("-rxs") result.stdout.fnmatch_lines( """ *XFAIL* @@ -1108,9 +1118,9 @@ def test_func(): ) -def test_xfail_item(testdir): +def test_xfail_item(pytester: Pytester) -> None: # Ensure pytest.xfail works with non-Python Item - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -1123,16 +1133,16 @@ def pytest_collect_file(path, parent): return MyItem.from_parent(name="foo", parent=parent) """ ) - result = testdir.inline_run() + result = pytester.inline_run() passed, skipped, failed = result.listoutcomes() assert not failed xfailed = [r for r in skipped if hasattr(r, "wasxfail")] assert xfailed -def test_module_level_skip_error(testdir): +def test_module_level_skip_error(pytester: Pytester) -> None: """Verify that using pytest.skip at module level causes a collection error.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest pytest.skip("skip_module_level") @@ -1141,15 +1151,15 @@ def test_func(): assert True """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( ["*Using pytest.skip outside of a test is not allowed*"] ) -def test_module_level_skip_with_allow_module_level(testdir): +def test_module_level_skip_with_allow_module_level(pytester: Pytester) -> None: """Verify that using pytest.skip(allow_module_level=True) is allowed.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest pytest.skip("skip_module_level", allow_module_level=True) @@ -1158,13 +1168,13 @@ def test_func(): assert 0 """ ) - result = testdir.runpytest("-rxs") + result = pytester.runpytest("-rxs") result.stdout.fnmatch_lines(["*SKIP*skip_module_level"]) -def test_invalid_skip_keyword_parameter(testdir): +def test_invalid_skip_keyword_parameter(pytester: Pytester) -> None: """Verify that using pytest.skip() with unknown parameter raises an error.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest pytest.skip("skip_module_level", unknown=1) @@ -1173,13 +1183,13 @@ def test_func(): assert 0 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*TypeError:*['unknown']*"]) -def test_mark_xfail_item(testdir): +def test_mark_xfail_item(pytester: Pytester) -> None: # Ensure pytest.mark.xfail works with non-Python Item - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -1197,23 +1207,23 @@ def pytest_collect_file(path, parent): return MyItem.from_parent(name="foo", parent=parent) """ ) - result = testdir.inline_run() + result = pytester.inline_run() passed, skipped, failed = result.listoutcomes() assert not failed xfailed = [r for r in skipped if hasattr(r, "wasxfail")] assert xfailed -def test_summary_list_after_errors(testdir): +def test_summary_list_after_errors(pytester: Pytester) -> None: """Ensure the list of errors/fails/xfails/skips appears after tracebacks in terminal reporting.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest def test_fail(): assert 0 """ ) - result = testdir.runpytest("-ra") + result = pytester.runpytest("-ra") result.stdout.fnmatch_lines( [ "=* FAILURES *=", @@ -1223,7 +1233,7 @@ def test_fail(): ) -def test_importorskip(): +def test_importorskip() -> None: with pytest.raises( pytest.skip.Exception, match="^could not import 'doesnotexist': No module named .*", @@ -1231,8 +1241,8 @@ def test_importorskip(): pytest.importorskip("doesnotexist") -def test_relpath_rootdir(testdir): - testdir.makepyfile( +def test_relpath_rootdir(pytester: Pytester) -> None: + pytester.makepyfile( **{ "tests/test_1.py": """ import pytest @@ -1242,7 +1252,7 @@ def test_pass(): """, } ) - result = testdir.runpytest("-rs", "tests/test_1.py", "--rootdir=tests") + result = pytester.runpytest("-rs", "tests/test_1.py", "--rootdir=tests") result.stdout.fnmatch_lines( ["SKIPPED [[]1[]] tests/test_1.py:2: unconditional skip"] ) From 5711ced250dc55f3516d3fd5a1d31f8345cfeb3e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 28 Oct 2020 14:21:33 -0300 Subject: [PATCH 733/823] Merge pull request #7958 from pytest-dev/release-6.1.2 Prepare release 6.1.2 (cherry picked from commit 1ed903e8fcbe60f8ce25b8911641059cd89d892b) --- changelog/7815.doc.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-6.1.2.rst | 22 ++++++++++++++++++++++ doc/en/changelog.rst | 19 +++++++++++++++++++ doc/en/getting-started.rst | 2 +- 5 files changed, 43 insertions(+), 2 deletions(-) delete mode 100644 changelog/7815.doc.rst create mode 100644 doc/en/announce/release-6.1.2.rst diff --git a/changelog/7815.doc.rst b/changelog/7815.doc.rst deleted file mode 100644 index d799bb42500..00000000000 --- a/changelog/7815.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Improve deprecation warning message for ``pytest._fillfuncargs()``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index bda389cd8be..f0e44107b69 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.1.2 release-6.1.1 release-6.1.0 release-6.0.2 diff --git a/doc/en/announce/release-6.1.2.rst b/doc/en/announce/release-6.1.2.rst new file mode 100644 index 00000000000..aa2c8095205 --- /dev/null +++ b/doc/en/announce/release-6.1.2.rst @@ -0,0 +1,22 @@ +pytest-6.1.2 +======================================= + +pytest 6.1.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Manuel Mariñez +* Ran Benita +* Vasilis Gerakaris +* William Jamir Silva + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 2a26b5c3fb9..8897ece8cb8 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,25 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 6.1.2 (2020-10-28) +========================= + +Bug Fixes +--------- + +- `#7758 `_: Fixed an issue where some files in packages are getting lost from ``--lf`` even though they contain tests that failed. Regressed in pytest 5.4.0. + + +- `#7911 `_: Directories created by `tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites. + + + +Improved Documentation +---------------------- + +- `#7815 `_: Improve deprecation warning message for ``pytest._fillfuncargs()``. + + pytest 6.1.1 (2020-10-03) ========================= diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 5a5f0fa7a43..3e4cdaf7220 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 6.1.1 + pytest 6.1.2 .. _`simpletest`: From de810152ec665e7a246bb3ceed2a8bfae5965e23 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 28 Oct 2020 22:16:25 +0200 Subject: [PATCH 734/823] tox: remove checkqa-mypy environment We run mypy through pre-commit, and we don't keep duplicate targets in tox for all of the other linters. Since this adds some (small) maintenance overhead, remove it. --- .pre-commit-config.yaml | 2 +- setup.cfg | 2 -- tox.ini | 13 ------------- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d351182eea..80774031c8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.790 # NOTE: keep this in sync with setup.cfg. + rev: v0.790 hooks: - id: mypy files: ^(src/|testing/) diff --git a/setup.cfg b/setup.cfg index 134a80b3efd..08d5853f2f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -62,8 +62,6 @@ console_scripts = py.test=pytest:console_main [options.extras_require] -checkqa-mypy = - mypy==0.790 testing = argcomplete hypothesis>=3.56 diff --git a/tox.ini b/tox.ini index 5a4a85e558d..6c8b8b0ad95 100644 --- a/tox.ini +++ b/tox.ini @@ -59,19 +59,6 @@ basepython = python3 deps = pre-commit>=1.11.0 commands = pre-commit run --all-files --show-diff-on-failure {posargs:} -[testenv:mypy] -extras = checkqa-mypy, testing -commands = mypy {posargs:src testing} - -[testenv:mypy-diff] -extras = checkqa-mypy, testing -deps = - lxml - diff-cover -commands = - -mypy --cobertura-xml-report {envtmpdir} {posargs:src testing} - diff-cover --fail-under=100 --compare-branch={env:DIFF_BRANCH:origin/{env:GITHUB_BASE_REF:master}} {envtmpdir}/cobertura.xml - [testenv:docs] basepython = python3 usedevelop = True From e3ce5d6b539a7d4cb5f06f097ac4eaceb43685b2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 28 Oct 2020 22:21:13 +0200 Subject: [PATCH 735/823] pre-commit: install typed dependencies in the mypy target Otherwise, mypy doesn't know about them and their types are considered Any. --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d351182eea..3a7b236bbb3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,6 +56,9 @@ repos: args: [] additional_dependencies: - iniconfig>=1.1.0 + - py>=1.8.2 + - attrs>=19.2.0 + - packaging - repo: local hooks: - id: rst From efe470bf1c65a145609be32ef9a04a6f825bb964 Mon Sep 17 00:00:00 2001 From: Christine Mecklenborg Date: Thu, 29 Oct 2020 02:54:34 -0500 Subject: [PATCH 736/823] Migrate test_assertrewrite.py from testdir to pytester (#7952) --- testing/test_assertrewrite.py | 403 ++++++++++++++++++---------------- 1 file changed, 210 insertions(+), 193 deletions(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 251e35684c8..58a31ab8d24 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -29,7 +29,7 @@ from _pytest.assertion.rewrite import rewrite_asserts from _pytest.config import ExitCode from _pytest.pathlib import make_numbered_dir -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester def rewrite(src: str) -> ast.Module: @@ -66,7 +66,7 @@ def getmsg( class TestAssertionRewrite: - def test_place_initial_imports(self): + def test_place_initial_imports(self) -> None: s = """'Doc string'\nother = stuff""" m = rewrite(s) assert isinstance(m.body[0], ast.Expr) @@ -115,19 +115,19 @@ def test_dont_rewrite(self) -> None: assert isinstance(m.body[1], ast.Assert) assert m.body[1].msg is None - def test_dont_rewrite_plugin(self, testdir): + def test_dont_rewrite_plugin(self, pytester: Pytester) -> None: contents = { "conftest.py": "pytest_plugins = 'plugin'; import plugin", "plugin.py": "'PYTEST_DONT_REWRITE'", "test_foo.py": "def test_foo(): pass", } - testdir.makepyfile(**contents) - result = testdir.runpytest_subprocess() + pytester.makepyfile(**contents) + result = pytester.runpytest_subprocess() assert "warning" not in "".join(result.outlines) - def test_rewrites_plugin_as_a_package(self, testdir): - pkgdir = testdir.mkpydir("plugin") - pkgdir.join("__init__.py").write( + def test_rewrites_plugin_as_a_package(self, pytester: Pytester) -> None: + pkgdir = pytester.mkpydir("plugin") + pkgdir.joinpath("__init__.py").write_text( "import pytest\n" "@pytest.fixture\n" "def special_asserter():\n" @@ -135,26 +135,27 @@ def test_rewrites_plugin_as_a_package(self, testdir): " assert x == y\n" " return special_assert\n" ) - testdir.makeconftest('pytest_plugins = ["plugin"]') - testdir.makepyfile("def test(special_asserter): special_asserter(1, 2)\n") - result = testdir.runpytest() + pytester.makeconftest('pytest_plugins = ["plugin"]') + pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n") + result = pytester.runpytest() result.stdout.fnmatch_lines(["*assert 1 == 2*"]) - def test_honors_pep_235(self, testdir, monkeypatch): + def test_honors_pep_235(self, pytester: Pytester, monkeypatch) -> None: # note: couldn't make it fail on macos with a single `sys.path` entry # note: these modules are named `test_*` to trigger rewriting - testdir.tmpdir.join("test_y.py").write("x = 1") - xdir = testdir.tmpdir.join("x").ensure_dir() - xdir.join("test_Y").ensure_dir().join("__init__.py").write("x = 2") - testdir.makepyfile( + pytester.makepyfile(test_y="x = 1") + xdir = pytester.mkdir("x") + pytester.mkpydir(str(xdir.joinpath("test_Y"))) + xdir.joinpath("test_Y").joinpath("__init__.py").write_text("x = 2") + pytester.makepyfile( "import test_y\n" "import test_Y\n" "def test():\n" " assert test_y.x == 1\n" " assert test_Y.x == 2\n" ) - monkeypatch.syspath_prepend(xdir) - testdir.runpytest().assert_outcomes(passed=1) + monkeypatch.syspath_prepend(str(xdir)) + pytester.runpytest().assert_outcomes(passed=1) def test_name(self, request) -> None: def f1() -> None: @@ -260,78 +261,78 @@ def f() -> None: " + where Y = cls()", ] - def test_assert_already_has_message(self): + def test_assert_already_has_message(self) -> None: def f(): assert False, "something bad!" assert getmsg(f) == "AssertionError: something bad!\nassert False" - def test_assertion_message(self, testdir): - testdir.makepyfile( + def test_assertion_message(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_foo(): assert 1 == 2, "The failure message" """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 1 result.stdout.fnmatch_lines( ["*AssertionError*The failure message*", "*assert 1 == 2*"] ) - def test_assertion_message_multiline(self, testdir): - testdir.makepyfile( + def test_assertion_message_multiline(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_foo(): assert 1 == 2, "A multiline\\nfailure message" """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 1 result.stdout.fnmatch_lines( ["*AssertionError*A multiline*", "*failure message*", "*assert 1 == 2*"] ) - def test_assertion_message_tuple(self, testdir): - testdir.makepyfile( + def test_assertion_message_tuple(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_foo(): assert 1 == 2, (1, 2) """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 1 result.stdout.fnmatch_lines( ["*AssertionError*%s*" % repr((1, 2)), "*assert 1 == 2*"] ) - def test_assertion_message_expr(self, testdir): - testdir.makepyfile( + def test_assertion_message_expr(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_foo(): assert 1 == 2, 1 + 2 """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 1 result.stdout.fnmatch_lines(["*AssertionError*3*", "*assert 1 == 2*"]) - def test_assertion_message_escape(self, testdir): - testdir.makepyfile( + def test_assertion_message_escape(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_foo(): assert 1 == 2, 'To be escaped: %' """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 1 result.stdout.fnmatch_lines( ["*AssertionError: To be escaped: %", "*assert 1 == 2"] ) - def test_assertion_messages_bytes(self, testdir): - testdir.makepyfile("def test_bytes_assertion():\n assert False, b'ohai!'\n") - result = testdir.runpytest() + def test_assertion_messages_bytes(self, pytester: Pytester) -> None: + pytester.makepyfile("def test_bytes_assertion():\n assert False, b'ohai!'\n") + result = pytester.runpytest() assert result.ret == 1 result.stdout.fnmatch_lines(["*AssertionError: b'ohai!'", "*assert False"]) @@ -475,8 +476,8 @@ def f2() -> None: assert getmsg(f2) == "assert (False or (4 % 2))" - def test_at_operator_issue1290(self, testdir): - testdir.makepyfile( + def test_at_operator_issue1290(self, pytester: Pytester) -> None: + pytester.makepyfile( """ class Matrix(object): def __init__(self, num): @@ -487,11 +488,11 @@ def __matmul__(self, other): def test_multmat_operator(): assert Matrix(2) @ Matrix(3) == 6""" ) - testdir.runpytest().assert_outcomes(passed=1) + pytester.runpytest().assert_outcomes(passed=1) - def test_starred_with_side_effect(self, testdir): + def test_starred_with_side_effect(self, pytester: Pytester) -> None: """See #4412""" - testdir.makepyfile( + pytester.makepyfile( """\ def test(): f = lambda x: x @@ -499,7 +500,7 @@ def test(): assert 2 * next(x) == f(*[next(x)]) """ ) - testdir.runpytest().assert_outcomes(passed=1) + pytester.runpytest().assert_outcomes(passed=1) def test_call(self) -> None: def g(a=42, *args, **kwargs) -> bool: @@ -629,7 +630,7 @@ def f5() -> None: getmsg(f5, must_pass=True) - def test_len(self, request): + def test_len(self, request) -> None: def f(): values = list(range(10)) assert len(values) == 11 @@ -727,31 +728,31 @@ def __repr__(self): class TestRewriteOnImport: - def test_pycache_is_a_file(self, testdir): - testdir.tmpdir.join("__pycache__").write("Hello") - testdir.makepyfile( + def test_pycache_is_a_file(self, pytester: Pytester) -> None: + pytester.path.joinpath("__pycache__").write_text("Hello") + pytester.makepyfile( """ def test_rewritten(): assert "@py_builtins" in globals()""" ) - assert testdir.runpytest().ret == 0 + assert pytester.runpytest().ret == 0 - def test_pycache_is_readonly(self, testdir): - cache = testdir.tmpdir.mkdir("__pycache__") - old_mode = cache.stat().mode + def test_pycache_is_readonly(self, pytester: Pytester) -> None: + cache = pytester.mkdir("__pycache__") + old_mode = cache.stat().st_mode cache.chmod(old_mode ^ stat.S_IWRITE) - testdir.makepyfile( + pytester.makepyfile( """ def test_rewritten(): assert "@py_builtins" in globals()""" ) try: - assert testdir.runpytest().ret == 0 + assert pytester.runpytest().ret == 0 finally: cache.chmod(old_mode) - def test_zipfile(self, testdir): - z = testdir.tmpdir.join("myzip.zip") + def test_zipfile(self, pytester: Pytester) -> None: + z = pytester.path.joinpath("myzip.zip") z_fn = str(z) f = zipfile.ZipFile(z_fn, "w") try: @@ -760,35 +761,34 @@ def test_zipfile(self, testdir): finally: f.close() z.chmod(256) - testdir.makepyfile( + pytester.makepyfile( """ import sys sys.path.append(%r) import test_gum.test_lizard""" % (z_fn,) ) - assert testdir.runpytest().ret == ExitCode.NO_TESTS_COLLECTED + assert pytester.runpytest().ret == ExitCode.NO_TESTS_COLLECTED - def test_readonly(self, testdir): - sub = testdir.mkdir("testing") - sub.join("test_readonly.py").write( + def test_readonly(self, pytester: Pytester) -> None: + sub = pytester.mkdir("testing") + sub.joinpath("test_readonly.py").write_bytes( b""" def test_rewritten(): assert "@py_builtins" in globals() """, - "wb", ) - old_mode = sub.stat().mode + old_mode = sub.stat().st_mode sub.chmod(320) try: - assert testdir.runpytest().ret == 0 + assert pytester.runpytest().ret == 0 finally: sub.chmod(old_mode) - def test_dont_write_bytecode(self, testdir, monkeypatch): + def test_dont_write_bytecode(self, pytester: Pytester, monkeypatch) -> None: monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) - testdir.makepyfile( + pytester.makepyfile( """ import os def test_no_bytecode(): @@ -797,20 +797,20 @@ def test_no_bytecode(): assert not os.path.exists(os.path.dirname(__cached__))""" ) monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") - assert testdir.runpytest_subprocess().ret == 0 + assert pytester.runpytest_subprocess().ret == 0 - def test_orphaned_pyc_file(self, testdir, monkeypatch): + def test_orphaned_pyc_file(self, pytester: Pytester, monkeypatch) -> None: monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) monkeypatch.setattr(sys, "pycache_prefix", None, raising=False) - testdir.makepyfile( + pytester.makepyfile( """ import orphan def test_it(): assert orphan.value == 17 """ ) - testdir.makepyfile( + pytester.makepyfile( orphan=""" value = 17 """ @@ -826,19 +826,21 @@ def test_it(): assert len(pycs) == 1 os.rename(pycs[0], "orphan.pyc") - assert testdir.runpytest().ret == 0 + assert pytester.runpytest().ret == 0 - def test_cached_pyc_includes_pytest_version(self, testdir, monkeypatch): + def test_cached_pyc_includes_pytest_version( + self, pytester: Pytester, monkeypatch + ) -> None: """Avoid stale caches (#1671)""" monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) - testdir.makepyfile( + pytester.makepyfile( test_foo=""" def test_foo(): assert True """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() assert result.ret == 0 found_names = glob.glob(f"__pycache__/*-pytest-{pytest.__version__}.pyc") assert found_names, "pyc with expected tag not found in names: {}".format( @@ -846,81 +848,83 @@ def test_foo(): ) @pytest.mark.skipif('"__pypy__" in sys.modules') - def test_pyc_vs_pyo(self, testdir, monkeypatch): - testdir.makepyfile( + def test_pyc_vs_pyo(self, pytester: Pytester, monkeypatch) -> None: + pytester.makepyfile( """ import pytest def test_optimized(): "hello" assert test_optimized.__doc__ is None""" ) - p = make_numbered_dir(root=Path(testdir.tmpdir), prefix="runpytest-") + p = make_numbered_dir(root=Path(pytester.path), prefix="runpytest-") tmp = "--basetemp=%s" % p monkeypatch.setenv("PYTHONOPTIMIZE", "2") monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) - assert testdir.runpytest_subprocess(tmp).ret == 0 + assert pytester.runpytest_subprocess(tmp).ret == 0 tagged = "test_pyc_vs_pyo." + PYTEST_TAG assert tagged + ".pyo" in os.listdir("__pycache__") monkeypatch.undo() monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) - assert testdir.runpytest_subprocess(tmp).ret == 1 + assert pytester.runpytest_subprocess(tmp).ret == 1 assert tagged + ".pyc" in os.listdir("__pycache__") - def test_package(self, testdir): - pkg = testdir.tmpdir.join("pkg") + def test_package(self, pytester: Pytester) -> None: + pkg = pytester.path.joinpath("pkg") pkg.mkdir() - pkg.join("__init__.py").ensure() - pkg.join("test_blah.py").write( + pkg.joinpath("__init__.py") + pkg.joinpath("test_blah.py").write_text( """ def test_rewritten(): assert "@py_builtins" in globals()""" ) - assert testdir.runpytest().ret == 0 + assert pytester.runpytest().ret == 0 - def test_translate_newlines(self, testdir): + def test_translate_newlines(self, pytester: Pytester) -> None: content = "def test_rewritten():\r\n assert '@py_builtins' in globals()" b = content.encode("utf-8") - testdir.tmpdir.join("test_newlines.py").write(b, "wb") - assert testdir.runpytest().ret == 0 + pytester.path.joinpath("test_newlines.py").write_bytes(b) + assert pytester.runpytest().ret == 0 - def test_package_without__init__py(self, testdir): - pkg = testdir.mkdir("a_package_without_init_py") - pkg.join("module.py").ensure() - testdir.makepyfile("import a_package_without_init_py.module") - assert testdir.runpytest().ret == ExitCode.NO_TESTS_COLLECTED + def test_package_without__init__py(self, pytester: Pytester) -> None: + pkg = pytester.mkdir("a_package_without_init_py") + pkg.joinpath("module.py").touch() + pytester.makepyfile("import a_package_without_init_py.module") + assert pytester.runpytest().ret == ExitCode.NO_TESTS_COLLECTED - def test_rewrite_warning(self, testdir): - testdir.makeconftest( + def test_rewrite_warning(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest pytest.register_assert_rewrite("_pytest") """ ) # needs to be a subprocess because pytester explicitly disables this warning - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*Module already imported*: _pytest"]) - def test_rewrite_module_imported_from_conftest(self, testdir): - testdir.makeconftest( + def test_rewrite_module_imported_from_conftest(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import test_rewrite_module_imported """ ) - testdir.makepyfile( + pytester.makepyfile( test_rewrite_module_imported=""" def test_rewritten(): assert "@py_builtins" in globals() """ ) - assert testdir.runpytest_subprocess().ret == 0 + assert pytester.runpytest_subprocess().ret == 0 - def test_remember_rewritten_modules(self, pytestconfig, testdir, monkeypatch): + def test_remember_rewritten_modules( + self, pytestconfig, pytester: Pytester, monkeypatch + ) -> None: """`AssertionRewriteHook` should remember rewritten modules so it doesn't give false positives (#2005).""" - monkeypatch.syspath_prepend(testdir.tmpdir) - testdir.makepyfile(test_remember_rewritten_modules="") + monkeypatch.syspath_prepend(pytester.path) + pytester.makepyfile(test_remember_rewritten_modules="") warnings = [] hook = AssertionRewritingHook(pytestconfig) monkeypatch.setattr( @@ -934,8 +938,8 @@ def test_remember_rewritten_modules(self, pytestconfig, testdir, monkeypatch): hook.mark_rewrite("test_remember_rewritten_modules") assert warnings == [] - def test_rewrite_warning_using_pytest_plugins(self, testdir): - testdir.makepyfile( + def test_rewrite_warning_using_pytest_plugins(self, pytester: Pytester) -> None: + pytester.makepyfile( **{ "conftest.py": "pytest_plugins = ['core', 'gui', 'sci']", "core.py": "", @@ -944,14 +948,16 @@ def test_rewrite_warning_using_pytest_plugins(self, testdir): "test_rewrite_warning_pytest_plugins.py": "def test(): pass", } ) - testdir.chdir() - result = testdir.runpytest_subprocess() + pytester.chdir() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*= 1 passed in *=*"]) result.stdout.no_fnmatch_line("*pytest-warning summary*") - def test_rewrite_warning_using_pytest_plugins_env_var(self, testdir, monkeypatch): + def test_rewrite_warning_using_pytest_plugins_env_var( + self, pytester: Pytester, monkeypatch + ) -> None: monkeypatch.setenv("PYTEST_PLUGINS", "plugin") - testdir.makepyfile( + pytester.makepyfile( **{ "plugin.py": "", "test_rewrite_warning_using_pytest_plugins_env_var.py": """ @@ -962,29 +968,30 @@ def test(): """, } ) - testdir.chdir() - result = testdir.runpytest_subprocess() + pytester.chdir() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*= 1 passed in *=*"]) result.stdout.no_fnmatch_line("*pytest-warning summary*") class TestAssertionRewriteHookDetails: - def test_sys_meta_path_munged(self, testdir): - testdir.makepyfile( + def test_sys_meta_path_munged(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_meta_path(): import sys; sys.meta_path = []""" ) - assert testdir.runpytest().ret == 0 + assert pytester.runpytest().ret == 0 - def test_write_pyc(self, testdir: Testdir, tmpdir, monkeypatch) -> None: + def test_write_pyc(self, pytester: Pytester, tmp_path, monkeypatch) -> None: from _pytest.assertion.rewrite import _write_pyc from _pytest.assertion import AssertionState - config = testdir.parseconfig() + config = pytester.parseconfig() state = AssertionState(config, "rewrite") - source_path = str(tmpdir.ensure("source.py")) - pycpath = tmpdir.join("pyc").strpath + tmp_path.joinpath("source.py").touch() + source_path = str(tmp_path) + pycpath = tmp_path.joinpath("pyc") co = compile("1", "f.py", "single") assert _write_pyc(state, co, os.stat(source_path), pycpath) @@ -1010,7 +1017,7 @@ def raise_oserror(*args): assert not _write_pyc(state, co, os.stat(source_path), pycpath) - def test_resources_provider_for_loader(self, testdir): + def test_resources_provider_for_loader(self, pytester: Pytester) -> None: """ Attempts to load resources from a package should succeed normally, even when the AssertionRewriteHook is used to load the modules. @@ -1019,7 +1026,7 @@ def test_resources_provider_for_loader(self, testdir): """ pytest.importorskip("pkg_resources") - testdir.mkpydir("testpkg") + pytester.mkpydir("testpkg") contents = { "testpkg/test_pkg": """ import pkg_resources @@ -1034,10 +1041,10 @@ def test_load_resource(): assert res == 'Load me please.' """ } - testdir.makepyfile(**contents) - testdir.maketxtfile(**{"testpkg/resource": "Load me please."}) + pytester.makepyfile(**contents) + pytester.maketxtfile(**{"testpkg/resource": "Load me please."}) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.assert_outcomes(passed=1) def test_read_pyc(self, tmp_path: Path) -> None: @@ -1062,15 +1069,15 @@ def test_read_pyc(self, tmp_path: Path) -> None: assert _read_pyc(source, pyc) is None # no error - def test_reload_is_same_and_reloads(self, testdir: Testdir) -> None: + def test_reload_is_same_and_reloads(self, pytester: Pytester) -> None: """Reloading a (collected) module after change picks up the change.""" - testdir.makeini( + pytester.makeini( """ [pytest] python_files = *.py """ ) - testdir.makepyfile( + pytester.makepyfile( file=""" def reloaded(): return False @@ -1091,13 +1098,13 @@ def test_loader(): assert file.reloaded() """, ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 1 passed*"]) - def test_get_data_support(self, testdir): + def test_get_data_support(self, pytester: Pytester) -> None: """Implement optional PEP302 api (#808).""" - path = testdir.mkpydir("foo") - path.join("test_foo.py").write( + path = pytester.mkpydir("foo") + path.joinpath("test_foo.py").write_text( textwrap.dedent( """\ class Test(object): @@ -1108,13 +1115,13 @@ def test_foo(self): """ ) ) - path.join("data.txt").write("Hey") - result = testdir.runpytest() + path.joinpath("data.txt").write_text("Hey") + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) -def test_issue731(testdir): - testdir.makepyfile( +def test_issue731(pytester: Pytester) -> None: + pytester.makepyfile( """ class LongReprWithBraces(object): def __repr__(self): @@ -1128,45 +1135,45 @@ def test_long_repr(): assert obj.some_method() """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.no_fnmatch_line("*unbalanced braces*") class TestIssue925: - def test_simple_case(self, testdir): - testdir.makepyfile( + def test_simple_case(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_ternary_display(): assert (False == False) == False """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*E*assert (False == False) == False"]) - def test_long_case(self, testdir): - testdir.makepyfile( + def test_long_case(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_ternary_display(): assert False == (False == True) == True """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*E*assert (False == True) == True"]) - def test_many_brackets(self, testdir): - testdir.makepyfile( + def test_many_brackets(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_ternary_display(): assert True == ((False == True) == True) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*E*assert True == ((False == True) == True)"]) class TestIssue2121: - def test_rewrite_python_files_contain_subdirs(self, testdir): - testdir.makepyfile( + def test_rewrite_python_files_contain_subdirs(self, pytester: Pytester) -> None: + pytester.makepyfile( **{ "tests/file.py": """ def test_simple_failure(): @@ -1174,13 +1181,13 @@ def test_simple_failure(): """ } ) - testdir.makeini( + pytester.makeini( """ [pytest] python_files = tests/**.py """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"]) @@ -1188,7 +1195,7 @@ def test_simple_failure(): sys.maxsize <= (2 ** 31 - 1), reason="Causes OverflowError on 32bit systems" ) @pytest.mark.parametrize("offset", [-1, +1]) -def test_source_mtime_long_long(testdir, offset): +def test_source_mtime_long_long(pytester: Pytester, offset) -> None: """Support modification dates after 2038 in rewritten files (#4903). pytest would crash with: @@ -1196,7 +1203,7 @@ def test_source_mtime_long_long(testdir, offset): fp.write(struct.pack(" None: +def test_rewrite_infinite_recursion( + pytester: Pytester, pytestconfig, monkeypatch +) -> None: """Fix infinite recursion when writing pyc files: if an import happens to be triggered when writing the pyc file, this would cause another call to the hook, which would trigger another pyc writing, which could trigger another import, and so on. (#3506)""" from _pytest.assertion import rewrite as rewritemod - testdir.syspathinsert() - testdir.makepyfile(test_foo="def test_foo(): pass") - testdir.makepyfile(test_bar="def test_bar(): pass") + pytester.syspathinsert() + pytester.makepyfile(test_foo="def test_foo(): pass") + pytester.makepyfile(test_bar="def test_bar(): pass") original_write_pyc = rewritemod._write_pyc @@ -1244,7 +1253,9 @@ def spy_write_pyc(*args, **kwargs): class TestEarlyRewriteBailout: @pytest.fixture - def hook(self, pytestconfig, monkeypatch, testdir) -> AssertionRewritingHook: + def hook( + self, pytestconfig, monkeypatch, pytester: Pytester + ) -> AssertionRewritingHook: """Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track if PathFinder.find_spec has been called. """ @@ -1268,25 +1279,25 @@ def spy_find_spec(name, path): hook.fnpats[:] = ["test_*.py", "*_test.py"] monkeypatch.setattr(hook, "_find_spec", spy_find_spec) hook.set_session(StubSession()) # type: ignore[arg-type] - testdir.syspathinsert() + pytester.syspathinsert() return hook - def test_basic(self, testdir, hook: AssertionRewritingHook) -> None: + def test_basic(self, pytester: Pytester, hook: AssertionRewritingHook) -> None: """ Ensure we avoid calling PathFinder.find_spec when we know for sure a certain module will not be rewritten to optimize assertion rewriting (#3918). """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @pytest.fixture def fix(): return 1 """ ) - testdir.makepyfile(test_foo="def test_foo(): pass") - testdir.makepyfile(bar="def bar(): pass") - foobar_path = testdir.makepyfile(foobar="def foobar(): pass") - self.initial_paths.add(foobar_path) + pytester.makepyfile(test_foo="def test_foo(): pass") + pytester.makepyfile(bar="def bar(): pass") + foobar_path = pytester.makepyfile(foobar="def foobar(): pass") + self.initial_paths.add(py.path.local(foobar_path)) # conftest files should always be rewritten assert hook.find_spec("conftest") is not None @@ -1305,12 +1316,12 @@ def fix(): return 1 assert self.find_spec_calls == ["conftest", "test_foo", "foobar"] def test_pattern_contains_subdirectories( - self, testdir, hook: AssertionRewritingHook + self, pytester: Pytester, hook: AssertionRewritingHook ) -> None: """If one of the python_files patterns contain subdirectories ("tests/**.py") we can't bailout early because we need to match with the full path, which can only be found by calling PathFinder.find_spec """ - p = testdir.makepyfile( + pytester.makepyfile( **{ "tests/file.py": """\ def test_simple_failure(): @@ -1318,7 +1329,7 @@ def test_simple_failure(): """ } ) - testdir.syspathinsert(p.dirpath()) + pytester.syspathinsert("tests") hook.fnpats[:] = ["tests/**.py"] assert hook.find_spec("file") is not None assert self.find_spec_calls == ["file"] @@ -1326,14 +1337,14 @@ def test_simple_failure(): @pytest.mark.skipif( sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" ) - def test_cwd_changed(self, testdir, monkeypatch): + def test_cwd_changed(self, pytester: Pytester, monkeypatch) -> None: # Setup conditions for py's fspath trying to import pathlib on py34 # always (previously triggered via xdist only). # Ref: https://github.com/pytest-dev/py/pull/207 monkeypatch.syspath_prepend("") monkeypatch.delitem(sys.modules, "pathlib", raising=False) - testdir.makepyfile( + pytester.makepyfile( **{ "test_setup_nonexisting_cwd.py": """\ import os @@ -1350,30 +1361,30 @@ def test(): """, } ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 1 passed in *"]) class TestAssertionPass: - def test_option_default(self, testdir): - config = testdir.parseconfig() + def test_option_default(self, pytester: Pytester) -> None: + config = pytester.parseconfig() assert config.getini("enable_assertion_pass_hook") is False @pytest.fixture - def flag_on(self, testdir): - testdir.makeini("[pytest]\nenable_assertion_pass_hook = True\n") + def flag_on(self, pytester: Pytester): + pytester.makeini("[pytest]\nenable_assertion_pass_hook = True\n") @pytest.fixture - def hook_on(self, testdir): - testdir.makeconftest( + def hook_on(self, pytester: Pytester): + pytester.makeconftest( """\ def pytest_assertion_pass(item, lineno, orig, expl): raise Exception("Assertion Passed: {} {} at line {}".format(orig, expl, lineno)) """ ) - def test_hook_call(self, testdir, flag_on, hook_on): - testdir.makepyfile( + def test_hook_call(self, pytester: Pytester, flag_on, hook_on) -> None: + pytester.makepyfile( """\ def test_simple(): a=1 @@ -1388,23 +1399,25 @@ def test_fails(): assert False, "assert with message" """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( "*Assertion Passed: a+b == c+d (1 + 2) == (3 + 0) at line 7*" ) - def test_hook_call_with_parens(self, testdir, flag_on, hook_on): - testdir.makepyfile( + def test_hook_call_with_parens(self, pytester: Pytester, flag_on, hook_on) -> None: + pytester.makepyfile( """\ def f(): return 1 def test(): assert f() """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines("*Assertion Passed: f() 1") - def test_hook_not_called_without_hookimpl(self, testdir, monkeypatch, flag_on): + def test_hook_not_called_without_hookimpl( + self, pytester: Pytester, monkeypatch, flag_on + ) -> None: """Assertion pass should not be called (and hence formatting should not occur) if there is no hook declared for pytest_assertion_pass""" @@ -1415,7 +1428,7 @@ def raise_on_assertionpass(*_, **__): _pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass ) - testdir.makepyfile( + pytester.makepyfile( """\ def test_simple(): a=1 @@ -1426,10 +1439,12 @@ def test_simple(): assert a+b == c+d """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=1) - def test_hook_not_called_without_cmd_option(self, testdir, monkeypatch): + def test_hook_not_called_without_cmd_option( + self, pytester: Pytester, monkeypatch + ) -> None: """Assertion pass should not be called (and hence formatting should not occur) if there is no hook declared for pytest_assertion_pass""" @@ -1440,14 +1455,14 @@ def raise_on_assertionpass(*_, **__): _pytest.assertion.rewrite, "_call_assertion_pass", raise_on_assertionpass ) - testdir.makeconftest( + pytester.makeconftest( """\ def pytest_assertion_pass(item, lineno, orig, expl): raise Exception("Assertion Passed: {} {} at line {}".format(orig, expl, lineno)) """ ) - testdir.makepyfile( + pytester.makepyfile( """\ def test_simple(): a=1 @@ -1458,7 +1473,7 @@ def test_simple(): assert a+b == c+d """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=1) @@ -1545,7 +1560,7 @@ def test_simple(): # fmt: on ), ) -def test_get_assertion_exprs(src, expected): +def test_get_assertion_exprs(src, expected) -> None: assert _get_assertion_exprs(src) == expected @@ -1599,7 +1614,7 @@ class TestPyCacheDir: (None, "/home/projects/src/foo.py", "/home/projects/src/__pycache__"), ], ) - def test_get_cache_dir(self, monkeypatch, prefix, source, expected): + 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): @@ -1611,13 +1626,15 @@ def test_get_cache_dir(self, monkeypatch, prefix, source, expected): @pytest.mark.skipif( sys.version_info < (3, 8), reason="pycache_prefix not available in py<38" ) - def test_sys_pycache_prefix_integration(self, tmp_path, monkeypatch, testdir): + def test_sys_pycache_prefix_integration( + self, tmp_path, monkeypatch, pytester: Pytester + ) -> None: """Integration test for sys.pycache_prefix (#4730).""" pycache_prefix = tmp_path / "my/pycs" monkeypatch.setattr(sys, "pycache_prefix", str(pycache_prefix)) monkeypatch.setattr(sys, "dont_write_bytecode", False) - testdir.makepyfile( + pytester.makepyfile( **{ "src/test_foo.py": """ import bar @@ -1627,11 +1644,11 @@ def test_foo(): "src/bar/__init__.py": "", } ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 - test_foo = Path(testdir.tmpdir) / "src/test_foo.py" - bar_init = Path(testdir.tmpdir) / "src/bar/__init__.py" + test_foo = pytester.path.joinpath("src/test_foo.py") + bar_init = pytester.path.joinpath("src/bar/__init__.py") assert test_foo.is_file() assert bar_init.is_file() From 460b51dd950ccea54f99d17553c3a42c127cc6a7 Mon Sep 17 00:00:00 2001 From: Christine Mecklenborg Date: Thu, 29 Oct 2020 02:55:30 -0500 Subject: [PATCH 737/823] Migrate test_setuponly.py from testdir to pytester (#7959) --- testing/test_setuponly.py | 81 ++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/testing/test_setuponly.py b/testing/test_setuponly.py index a43c850696e..fe4bdc514eb 100644 --- a/testing/test_setuponly.py +++ b/testing/test_setuponly.py @@ -2,6 +2,7 @@ import pytest from _pytest.config import ExitCode +from _pytest.pytester import Pytester @pytest.fixture(params=["--setup-only", "--setup-plan", "--setup-show"], scope="module") @@ -9,8 +10,10 @@ def mode(request): return request.param -def test_show_only_active_fixtures(testdir, mode, dummy_yaml_custom_test): - testdir.makepyfile( +def test_show_only_active_fixtures( + pytester: Pytester, mode, dummy_yaml_custom_test +) -> None: + pytester.makepyfile( ''' import pytest @pytest.fixture @@ -24,7 +27,7 @@ def test_arg1(arg1): ''' ) - result = testdir.runpytest(mode) + result = pytester.runpytest(mode) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -33,8 +36,8 @@ def test_arg1(arg1): result.stdout.no_fnmatch_line("*_arg0*") -def test_show_different_scopes(testdir, mode): - p = testdir.makepyfile( +def test_show_different_scopes(pytester: Pytester, mode) -> None: + p = pytester.makepyfile( ''' import pytest @pytest.fixture @@ -48,7 +51,7 @@ def test_arg1(arg_session, arg_function): ''' ) - result = testdir.runpytest(mode, p) + result = pytester.runpytest(mode, p) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -62,8 +65,8 @@ def test_arg1(arg_session, arg_function): ) -def test_show_nested_fixtures(testdir, mode): - testdir.makeconftest( +def test_show_nested_fixtures(pytester: Pytester, mode) -> None: + pytester.makeconftest( ''' import pytest @pytest.fixture(scope='session') @@ -71,7 +74,7 @@ def arg_same(): """session scoped fixture""" ''' ) - p = testdir.makepyfile( + p = pytester.makepyfile( ''' import pytest @pytest.fixture(scope='function') @@ -82,7 +85,7 @@ def test_arg1(arg_same): ''' ) - result = testdir.runpytest(mode, p) + result = pytester.runpytest(mode, p) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -96,8 +99,8 @@ def test_arg1(arg_same): ) -def test_show_fixtures_with_autouse(testdir, mode): - p = testdir.makepyfile( +def test_show_fixtures_with_autouse(pytester: Pytester, mode) -> None: + p = pytester.makepyfile( ''' import pytest @pytest.fixture @@ -111,7 +114,7 @@ def test_arg1(arg_function): ''' ) - result = testdir.runpytest(mode, p) + result = pytester.runpytest(mode, p) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -123,8 +126,8 @@ def test_arg1(arg_function): ) -def test_show_fixtures_with_parameters(testdir, mode): - testdir.makeconftest( +def test_show_fixtures_with_parameters(pytester: Pytester, mode) -> None: + pytester.makeconftest( ''' import pytest @pytest.fixture(scope='session', params=['foo', 'bar']) @@ -132,7 +135,7 @@ def arg_same(): """session scoped fixture""" ''' ) - p = testdir.makepyfile( + p = pytester.makepyfile( ''' import pytest @pytest.fixture(scope='function') @@ -143,7 +146,7 @@ def test_arg1(arg_other): ''' ) - result = testdir.runpytest(mode, p) + result = pytester.runpytest(mode, p) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -156,8 +159,8 @@ def test_arg1(arg_other): ) -def test_show_fixtures_with_parameter_ids(testdir, mode): - testdir.makeconftest( +def test_show_fixtures_with_parameter_ids(pytester: Pytester, mode) -> None: + pytester.makeconftest( ''' import pytest @pytest.fixture( @@ -166,7 +169,7 @@ def arg_same(): """session scoped fixture""" ''' ) - p = testdir.makepyfile( + p = pytester.makepyfile( ''' import pytest @pytest.fixture(scope='function') @@ -177,7 +180,7 @@ def test_arg1(arg_other): ''' ) - result = testdir.runpytest(mode, p) + result = pytester.runpytest(mode, p) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -185,8 +188,8 @@ def test_arg1(arg_other): ) -def test_show_fixtures_with_parameter_ids_function(testdir, mode): - p = testdir.makepyfile( +def test_show_fixtures_with_parameter_ids_function(pytester: Pytester, mode) -> None: + p = pytester.makepyfile( """ import pytest @pytest.fixture(params=['foo', 'bar'], ids=lambda p: p.upper()) @@ -197,7 +200,7 @@ def test_foobar(foobar): """ ) - result = testdir.runpytest(mode, p) + result = pytester.runpytest(mode, p) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -205,8 +208,8 @@ def test_foobar(foobar): ) -def test_dynamic_fixture_request(testdir): - p = testdir.makepyfile( +def test_dynamic_fixture_request(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest @pytest.fixture() @@ -220,7 +223,7 @@ def test_dyn(dependent_fixture): """ ) - result = testdir.runpytest("--setup-only", p) + result = pytester.runpytest("--setup-only", p) assert result.ret == 0 result.stdout.fnmatch_lines( @@ -231,8 +234,8 @@ def test_dyn(dependent_fixture): ) -def test_capturing(testdir): - p = testdir.makepyfile( +def test_capturing(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest, sys @pytest.fixture() @@ -247,15 +250,15 @@ def test_capturing(two): """ ) - result = testdir.runpytest("--setup-only", p) + result = pytester.runpytest("--setup-only", p) result.stdout.fnmatch_lines( ["this should be captured", "this should also be captured"] ) -def test_show_fixtures_and_execute_test(testdir): +def test_show_fixtures_and_execute_test(pytester: Pytester) -> None: """Verify that setups are shown and tests are executed.""" - p = testdir.makepyfile( + p = pytester.makepyfile( """ import pytest @pytest.fixture @@ -266,7 +269,7 @@ def test_arg(arg): """ ) - result = testdir.runpytest("--setup-show", p) + result = pytester.runpytest("--setup-show", p) assert result.ret == 1 result.stdout.fnmatch_lines( @@ -274,8 +277,8 @@ def test_arg(arg): ) -def test_setup_show_with_KeyboardInterrupt_in_test(testdir): - p = testdir.makepyfile( +def test_setup_show_with_KeyboardInterrupt_in_test(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest @pytest.fixture @@ -285,7 +288,7 @@ def test_arg(arg): raise KeyboardInterrupt() """ ) - result = testdir.runpytest("--setup-show", p, no_reraise_ctrlc=True) + result = pytester.runpytest("--setup-show", p, no_reraise_ctrlc=True) result.stdout.fnmatch_lines( [ "*SETUP F arg*", @@ -298,9 +301,9 @@ def test_arg(arg): assert result.ret == ExitCode.INTERRUPTED -def test_show_fixture_action_with_bytes(testdir): +def test_show_fixture_action_with_bytes(pytester: Pytester) -> None: # Issue 7126, BytesWarning when using --setup-show with bytes parameter - test_file = testdir.makepyfile( + test_file = pytester.makepyfile( """ import pytest @@ -309,7 +312,7 @@ def test_data(data): pass """ ) - result = testdir.run( + result = pytester.run( sys.executable, "-bb", "-m", "pytest", "--setup-show", str(test_file) ) assert result.ret == 0 From 65148e312024d4e21bf4004d24f42bbe43505b5f Mon Sep 17 00:00:00 2001 From: Christine Mecklenborg Date: Thu, 29 Oct 2020 02:56:34 -0500 Subject: [PATCH 738/823] Migrate test_compat.py from testdir to pytester (#7963) --- testing/test_compat.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/testing/test_compat.py b/testing/test_compat.py index 5239b92c74b..9f48a31d689 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -13,12 +13,13 @@ from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass from _pytest.outcomes import OutcomeException +from _pytest.pytester import Pytester if TYPE_CHECKING: from typing_extensions import Literal -def test_is_generator(): +def test_is_generator() -> None: def zap(): yield # pragma: no cover @@ -29,7 +30,7 @@ def foo(): assert not is_generator(foo) -def test_real_func_loop_limit(): +def test_real_func_loop_limit() -> None: class Evil: def __init__(self): self.left = 1000 @@ -55,7 +56,7 @@ def __getattr__(self, attr): get_real_func(evil) -def test_get_real_func(): +def test_get_real_func() -> None: """Check that get_real_func correctly unwraps decorators until reaching the real function""" def decorator(f): @@ -80,7 +81,7 @@ def func(): assert get_real_func(wrapped_func2) is wrapped_func -def test_get_real_func_partial(): +def test_get_real_func_partial() -> None: """Test get_real_func handles partial instances correctly""" def foo(x): @@ -90,8 +91,8 @@ def foo(x): assert get_real_func(partial(foo)) is foo -def test_is_generator_asyncio(testdir): - testdir.makepyfile( +def test_is_generator_asyncio(pytester: Pytester) -> None: + pytester.makepyfile( """ from _pytest.compat import is_generator import asyncio @@ -105,12 +106,12 @@ def test_is_generator_asyncio(): ) # avoid importing asyncio into pytest's own process, # which in turn imports logging (#8) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 passed*"]) -def test_is_generator_async_syntax(testdir): - testdir.makepyfile( +def test_is_generator_async_syntax(pytester: Pytester) -> None: + pytester.makepyfile( """ from _pytest.compat import is_generator def test_is_generator_py35(): @@ -124,12 +125,12 @@ async def bar(): assert not is_generator(bar) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) -def test_is_generator_async_gen_syntax(testdir): - testdir.makepyfile( +def test_is_generator_async_gen_syntax(pytester: Pytester) -> None: + pytester.makepyfile( """ from _pytest.compat import is_generator def test_is_generator_py36(): @@ -144,7 +145,7 @@ async def bar(): assert not is_generator(bar) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) @@ -162,7 +163,7 @@ def raise_fail_outcome(self): pytest.fail("fail should be catched") -def test_helper_failures(): +def test_helper_failures() -> None: helper = ErrorsHelper() with pytest.raises(Exception): helper.raise_exception @@ -170,7 +171,7 @@ def test_helper_failures(): helper.raise_fail_outcome -def test_safe_getattr(): +def test_safe_getattr() -> None: helper = ErrorsHelper() assert safe_getattr(helper, "raise_exception", "default") == "default" assert safe_getattr(helper, "raise_fail_outcome", "default") == "default" @@ -178,7 +179,7 @@ def test_safe_getattr(): assert safe_getattr(helper, "raise_baseexception", "default") -def test_safe_isclass(): +def test_safe_isclass() -> None: assert safe_isclass(type) is True class CrappyClass(Exception): From 47ff911c8ff3e705ead23b183530ad161a6261ba Mon Sep 17 00:00:00 2001 From: Christine Mecklenborg Date: Thu, 29 Oct 2020 20:39:44 -0500 Subject: [PATCH 739/823] Migrate test_assertion.py from testdir to pytester --- testing/test_assertion.py | 343 ++++++++++++++++++++------------------ 1 file changed, 178 insertions(+), 165 deletions(-) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index e3b6fa51906..02ecaf125e1 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -12,6 +12,7 @@ from _pytest import outcomes from _pytest.assertion import truncate from _pytest.assertion import util +from _pytest.pytester import Pytester def mock_config(verbose=0): @@ -27,9 +28,12 @@ def getoption(self, name): class TestImportHookInstallation: @pytest.mark.parametrize("initial_conftest", [True, False]) @pytest.mark.parametrize("mode", ["plain", "rewrite"]) - def test_conftest_assertion_rewrite(self, testdir, initial_conftest, mode): + def test_conftest_assertion_rewrite( + self, pytester: Pytester, initial_conftest, mode + ) -> None: """Test that conftest files are using assertion rewrite on import (#1619).""" - testdir.tmpdir.join("foo/tests").ensure(dir=1) + pytester.mkdir("foo") + pytester.mkdir("foo/tests") conftest_path = "conftest.py" if initial_conftest else "foo/conftest.py" contents = { conftest_path: """ @@ -45,8 +49,8 @@ def test(check_first): check_first([10, 30], 30) """, } - testdir.makepyfile(**contents) - result = testdir.runpytest_subprocess("--assert=%s" % mode) + pytester.makepyfile(**contents) + result = pytester.runpytest_subprocess("--assert=%s" % mode) if mode == "plain": expected = "E AssertionError" elif mode == "rewrite": @@ -55,21 +59,21 @@ def test(check_first): assert 0 result.stdout.fnmatch_lines([expected]) - def test_rewrite_assertions_pytester_plugin(self, testdir): + def test_rewrite_assertions_pytester_plugin(self, pytester: Pytester) -> None: """ Assertions in the pytester plugin must also benefit from assertion rewriting (#1920). """ - testdir.makepyfile( + pytester.makepyfile( """ pytest_plugins = ['pytester'] - def test_dummy_failure(testdir): # how meta! - testdir.makepyfile('def test(): assert 0') - r = testdir.inline_run() + def test_dummy_failure(pytester): # how meta! + pytester.makepyfile('def test(): assert 0') + r = pytester.inline_run() r.assertoutcome(passed=1) """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines( [ "> r.assertoutcome(passed=1)", @@ -89,7 +93,7 @@ def test_dummy_failure(testdir): # how meta! ) @pytest.mark.parametrize("mode", ["plain", "rewrite"]) - def test_pytest_plugins_rewrite(self, testdir, mode): + def test_pytest_plugins_rewrite(self, pytester: Pytester, mode) -> None: contents = { "conftest.py": """ pytest_plugins = ['ham'] @@ -107,8 +111,8 @@ def test_foo(check_first): check_first([10, 30], 30) """, } - testdir.makepyfile(**contents) - result = testdir.runpytest_subprocess("--assert=%s" % mode) + pytester.makepyfile(**contents) + result = pytester.runpytest_subprocess("--assert=%s" % mode) if mode == "plain": expected = "E AssertionError" elif mode == "rewrite": @@ -118,7 +122,9 @@ def test_foo(check_first): result.stdout.fnmatch_lines([expected]) @pytest.mark.parametrize("mode", ["str", "list"]) - def test_pytest_plugins_rewrite_module_names(self, testdir, mode): + def test_pytest_plugins_rewrite_module_names( + self, pytester: Pytester, mode + ) -> None: """Test that pluginmanager correct marks pytest_plugins variables for assertion rewriting if they are defined as plain strings or list of strings (#1888). @@ -138,11 +144,13 @@ def test_foo(pytestconfig): assert 'ham' in pytestconfig.pluginmanager.rewrite_hook._must_rewrite """, } - testdir.makepyfile(**contents) - result = testdir.runpytest_subprocess("--assert=rewrite") + pytester.makepyfile(**contents) + result = pytester.runpytest_subprocess("--assert=rewrite") assert result.ret == 0 - def test_pytest_plugins_rewrite_module_names_correctly(self, testdir): + def test_pytest_plugins_rewrite_module_names_correctly( + self, pytester: Pytester + ) -> None: """Test that we match files correctly when they are marked for rewriting (#2939).""" contents = { "conftest.py": """\ @@ -156,16 +164,18 @@ def test_foo(pytestconfig): assert pytestconfig.pluginmanager.rewrite_hook.find_spec('hamster') is None """, } - testdir.makepyfile(**contents) - result = testdir.runpytest_subprocess("--assert=rewrite") + pytester.makepyfile(**contents) + result = pytester.runpytest_subprocess("--assert=rewrite") assert result.ret == 0 @pytest.mark.parametrize("mode", ["plain", "rewrite"]) - def test_installed_plugin_rewrite(self, testdir, mode, monkeypatch): + def test_installed_plugin_rewrite( + self, pytester: Pytester, mode, monkeypatch + ) -> None: monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) # Make sure the hook is installed early enough so that plugins # installed via setuptools are rewritten. - testdir.tmpdir.join("hampkg").ensure(dir=1) + pytester.mkdir("hampkg") contents = { "hampkg/__init__.py": """\ import pytest @@ -219,8 +229,8 @@ def test2(check_first2): check_first([10, 30], 30) """, } - testdir.makepyfile(**contents) - result = testdir.run( + pytester.makepyfile(**contents) + result = pytester.run( sys.executable, "mainwrapper.py", "-s", "--assert=%s" % mode ) if mode == "plain": @@ -231,8 +241,8 @@ def test2(check_first2): assert 0 result.stdout.fnmatch_lines([expected]) - def test_rewrite_ast(self, testdir): - testdir.tmpdir.join("pkg").ensure(dir=1) + def test_rewrite_ast(self, pytester: Pytester) -> None: + pytester.mkdir("pkg") contents = { "pkg/__init__.py": """ import pytest @@ -265,8 +275,8 @@ def test_other(): pkg.other.tool() """, } - testdir.makepyfile(**contents) - result = testdir.runpytest_subprocess("--assert=rewrite") + pytester.makepyfile(**contents) + result = pytester.runpytest_subprocess("--assert=rewrite") result.stdout.fnmatch_lines( [ ">*assert a == b*", @@ -285,8 +295,8 @@ def test_register_assert_rewrite_checks_types(self) -> None: class TestBinReprIntegration: - def test_pytest_assertrepr_compare_called(self, testdir): - testdir.makeconftest( + def test_pytest_assertrepr_compare_called(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest values = [] @@ -298,7 +308,7 @@ def list(request): return values """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_hello(): assert 0 == 1 @@ -306,7 +316,7 @@ def test_check(list): assert list == [("==", 0, 1)] """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"]) @@ -320,7 +330,7 @@ def callequal(left: Any, right: Any, verbose: int = 0) -> Optional[List[str]]: class TestAssert_reprcompare: - def test_different_types(self): + def test_different_types(self) -> None: assert callequal([0, 1], "foo") is None def test_summary(self) -> None: @@ -329,7 +339,7 @@ def test_summary(self) -> None: summary = lines[0] assert len(summary) < 65 - def test_text_diff(self): + def test_text_diff(self) -> None: assert callequal("spam", "eggs") == [ "'spam' == 'eggs'", "- eggs", @@ -357,7 +367,7 @@ def test_multiline_text_diff(self) -> None: assert "- eggs" in diff assert "+ spam" in diff - def test_bytes_diff_normal(self): + def test_bytes_diff_normal(self) -> None: """Check special handling for bytes diff (#5260)""" diff = callequal(b"spam", b"eggs") @@ -367,7 +377,7 @@ def test_bytes_diff_normal(self): "Use -v to get the full diff", ] - def test_bytes_diff_verbose(self): + def test_bytes_diff_verbose(self) -> None: """Check special handling for bytes diff (#5260)""" diff = callequal(b"spam", b"eggs", verbose=1) assert diff == [ @@ -445,7 +455,7 @@ def test_list_different_lengths(self) -> None: assert expl is not None assert len(expl) > 1 - def test_list_wrap_for_multiple_lines(self): + def test_list_wrap_for_multiple_lines(self) -> None: long_d = "d" * 80 l1 = ["a", "b", "c"] l2 = ["a", "b", "c", long_d] @@ -475,7 +485,7 @@ def test_list_wrap_for_multiple_lines(self): " ]", ] - def test_list_wrap_for_width_rewrap_same_length(self): + def test_list_wrap_for_width_rewrap_same_length(self) -> None: long_a = "a" * 30 long_b = "b" * 30 long_c = "c" * 30 @@ -494,7 +504,7 @@ def test_list_wrap_for_width_rewrap_same_length(self): " ]", ] - def test_list_dont_wrap_strings(self): + def test_list_dont_wrap_strings(self) -> None: long_a = "a" * 10 l1 = ["a"] + [long_a for _ in range(0, 7)] l2 = ["should not get wrapped"] @@ -517,7 +527,7 @@ def test_list_dont_wrap_strings(self): " ]", ] - def test_dict_wrap(self): + def test_dict_wrap(self) -> None: d1 = {"common": 1, "env": {"env1": 1, "env2": 2}} d2 = {"common": 1, "env": {"env1": 1}} @@ -581,7 +591,7 @@ def test_dict_omitting_with_verbosity_2(self) -> None: assert "Omitting" not in lines[1] assert lines[2] == "{'b': 1}" - def test_dict_different_items(self): + def test_dict_different_items(self) -> None: lines = callequal({"a": 0}, {"b": 1, "c": 2}, verbose=2) assert lines == [ "{'a': 0} == {'b': 1, 'c': 2}", @@ -605,7 +615,7 @@ def test_dict_different_items(self): "+ {'b': 1, 'c': 2}", ] - def test_sequence_different_items(self): + def test_sequence_different_items(self) -> None: lines = callequal((1, 2), (3, 4, 5), verbose=2) assert lines == [ "(1, 2) == (3, 4, 5)", @@ -714,7 +724,7 @@ def __repr__(self): " Probably an object has a faulty __repr__.)", ] - def test_one_repr_empty(self): + def test_one_repr_empty(self) -> None: """The faulty empty string repr did trigger an unbound local error in _diff_text.""" class A(str): @@ -729,14 +739,14 @@ def test_repr_no_exc(self) -> None: assert expl is not None assert "raised in repr()" not in " ".join(expl) - def test_unicode(self): + def test_unicode(self) -> None: assert callequal("£€", "£") == [ "'£€' == '£'", "- £", "+ £€", ] - def test_nonascii_text(self): + def test_nonascii_text(self) -> None: """ :issue: 877 non ascii python2 str caused a UnicodeDecodeError @@ -749,7 +759,7 @@ def __repr__(self): expl = callequal(A(), "1") assert expl == ["ÿ == '1'", "- 1"] - def test_format_nonascii_explanation(self): + def test_format_nonascii_explanation(self) -> None: assert util.format_explanation("λ") def test_mojibake(self) -> None: @@ -766,9 +776,9 @@ def test_mojibake(self) -> None: class TestAssert_reprcompare_dataclass: @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") - def test_dataclasses(self, testdir): - p = testdir.copy_example("dataclasses/test_compare_dataclasses.py") - result = testdir.runpytest(p) + def test_dataclasses(self, pytester: Pytester) -> None: + p = pytester.copy_example("dataclasses/test_compare_dataclasses.py") + result = pytester.runpytest(p) result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ @@ -785,9 +795,9 @@ def test_dataclasses(self, testdir): ) @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") - def test_recursive_dataclasses(self, testdir): - p = testdir.copy_example("dataclasses/test_compare_recursive_dataclasses.py") - result = testdir.runpytest(p) + def test_recursive_dataclasses(self, pytester: Pytester) -> None: + p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") + result = pytester.runpytest(p) result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ @@ -804,9 +814,9 @@ def test_recursive_dataclasses(self, testdir): ) @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") - def test_recursive_dataclasses_verbose(self, testdir): - p = testdir.copy_example("dataclasses/test_compare_recursive_dataclasses.py") - result = testdir.runpytest(p, "-vv") + def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None: + p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") + result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ @@ -837,9 +847,9 @@ def test_recursive_dataclasses_verbose(self, testdir): ) @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") - def test_dataclasses_verbose(self, testdir): - p = testdir.copy_example("dataclasses/test_compare_dataclasses_verbose.py") - result = testdir.runpytest(p, "-vv") + def test_dataclasses_verbose(self, pytester: Pytester) -> None: + p = pytester.copy_example("dataclasses/test_compare_dataclasses_verbose.py") + result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ @@ -851,19 +861,21 @@ def test_dataclasses_verbose(self, testdir): ) @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") - def test_dataclasses_with_attribute_comparison_off(self, testdir): - p = testdir.copy_example( + def test_dataclasses_with_attribute_comparison_off( + self, pytester: Pytester + ) -> None: + p = pytester.copy_example( "dataclasses/test_compare_dataclasses_field_comparison_off.py" ) - result = testdir.runpytest(p, "-vv") + result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=0, passed=1) @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") - def test_comparing_two_different_data_classes(self, testdir): - p = testdir.copy_example( + def test_comparing_two_different_data_classes(self, pytester: Pytester) -> None: + p = pytester.copy_example( "dataclasses/test_compare_two_different_dataclasses.py" ) - result = testdir.runpytest(p, "-vv") + result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=0, passed=1) @@ -939,7 +951,7 @@ class SimpleDataObject: assert "Omitting" not in lines[2] assert lines[3] == "['field_a']" - def test_attrs_with_attribute_comparison_off(self): + def test_attrs_with_attribute_comparison_off(self) -> None: @attr.s class SimpleDataObject: field_a = attr.ib() @@ -957,7 +969,7 @@ class SimpleDataObject: for line in lines[3:]: assert "field_b" not in line - def test_comparing_two_different_attrs_classes(self): + def test_comparing_two_different_attrs_classes(self) -> None: @attr.s class SimpleDataObjectOne: field_a = attr.ib() @@ -976,48 +988,48 @@ class SimpleDataObjectTwo: class TestFormatExplanation: - def test_special_chars_full(self, testdir): + def test_special_chars_full(self, pytester: Pytester) -> None: # Issue 453, for the bug this would raise IndexError - testdir.makepyfile( + pytester.makepyfile( """ def test_foo(): assert '\\n}' == '' """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 1 result.stdout.fnmatch_lines(["*AssertionError*"]) - def test_fmt_simple(self): + def test_fmt_simple(self) -> None: expl = "assert foo" assert util.format_explanation(expl) == "assert foo" - def test_fmt_where(self): + def test_fmt_where(self) -> None: expl = "\n".join(["assert 1", "{1 = foo", "} == 2"]) res = "\n".join(["assert 1 == 2", " + where 1 = foo"]) assert util.format_explanation(expl) == res - def test_fmt_and(self): + def test_fmt_and(self) -> None: expl = "\n".join(["assert 1", "{1 = foo", "} == 2", "{2 = bar", "}"]) res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + and 2 = bar"]) assert util.format_explanation(expl) == res - def test_fmt_where_nested(self): + def test_fmt_where_nested(self) -> None: expl = "\n".join(["assert 1", "{1 = foo", "{foo = bar", "}", "} == 2"]) res = "\n".join(["assert 1 == 2", " + where 1 = foo", " + where foo = bar"]) assert util.format_explanation(expl) == res - def test_fmt_newline(self): + def test_fmt_newline(self) -> None: expl = "\n".join(['assert "foo" == "bar"', "~- foo", "~+ bar"]) res = "\n".join(['assert "foo" == "bar"', " - foo", " + bar"]) assert util.format_explanation(expl) == res - def test_fmt_newline_escaped(self): + def test_fmt_newline_escaped(self) -> None: expl = "\n".join(["assert foo == bar", "baz"]) res = "assert foo == bar\\nbaz" assert util.format_explanation(expl) == res - def test_fmt_newline_before_where(self): + def test_fmt_newline_before_where(self) -> None: expl = "\n".join( [ "the assertion message here", @@ -1038,7 +1050,7 @@ def test_fmt_newline_before_where(self): ) assert util.format_explanation(expl) == res - def test_fmt_multi_newline_before_where(self): + def test_fmt_multi_newline_before_where(self) -> None: expl = "\n".join( [ "the assertion", @@ -1072,12 +1084,12 @@ def test_doesnt_truncate_when_input_is_empty_list(self) -> None: result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result == expl - def test_doesnt_truncate_at_when_input_is_5_lines_and_LT_max_chars(self): + def test_doesnt_truncate_at_when_input_is_5_lines_and_LT_max_chars(self) -> None: expl = ["a" * 100 for x in range(5)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result == expl - def test_truncates_at_8_lines_when_given_list_of_empty_strings(self): + def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: expl = ["" for x in range(50)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result != expl @@ -1087,7 +1099,7 @@ def test_truncates_at_8_lines_when_given_list_of_empty_strings(self): last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1] assert last_line_before_trunc_msg.endswith("...") - def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self): + def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: expl = ["a" for x in range(100)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result != expl @@ -1097,7 +1109,7 @@ def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self): last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1] assert last_line_before_trunc_msg.endswith("...") - def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self): + def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: expl = ["a" * 80 for x in range(16)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result != expl @@ -1107,7 +1119,7 @@ def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self): last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1] assert last_line_before_trunc_msg.endswith("...") - def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self): + def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None: expl = ["a" * 250 for x in range(10)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=999) assert result != expl @@ -1117,7 +1129,7 @@ def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self): last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1] assert last_line_before_trunc_msg.endswith("...") - def test_truncates_at_1_line_when_first_line_is_GT_max_chars(self): + def test_truncates_at_1_line_when_first_line_is_GT_max_chars(self) -> None: expl = ["a" * 250 for x in range(1000)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result != expl @@ -1127,13 +1139,13 @@ def test_truncates_at_1_line_when_first_line_is_GT_max_chars(self): last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1] assert last_line_before_trunc_msg.endswith("...") - def test_full_output_truncated(self, monkeypatch, testdir): + def test_full_output_truncated(self, monkeypatch, pytester: Pytester) -> None: """Test against full runpytest() output.""" line_count = 7 line_len = 100 expected_truncated_lines = 2 - testdir.makepyfile( + pytester.makepyfile( r""" def test_many_lines(): a = list([str(i)[0] * %d for i in range(%d)]) @@ -1146,7 +1158,7 @@ def test_many_lines(): ) monkeypatch.delenv("CI", raising=False) - result = testdir.runpytest() + result = pytester.runpytest() # without -vv, truncate the message showing a few diff lines only result.stdout.fnmatch_lines( [ @@ -1157,23 +1169,23 @@ def test_many_lines(): ] ) - result = testdir.runpytest("-vv") + result = pytester.runpytest("-vv") result.stdout.fnmatch_lines(["* 6*"]) monkeypatch.setenv("CI", "1") - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 6*"]) -def test_python25_compile_issue257(testdir): - testdir.makepyfile( +def test_python25_compile_issue257(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_rewritten(): assert 1 == 2 # some comment """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 1 result.stdout.fnmatch_lines( """ @@ -1183,14 +1195,14 @@ def test_rewritten(): ) -def test_rewritten(testdir): - testdir.makepyfile( +def test_rewritten(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_rewritten(): assert "@py_builtins" in globals() """ ) - assert testdir.runpytest().ret == 0 + assert pytester.runpytest().ret == 0 def test_reprcompare_notin() -> None: @@ -1202,7 +1214,7 @@ def test_reprcompare_notin() -> None: ] -def test_reprcompare_whitespaces(): +def test_reprcompare_whitespaces() -> None: assert callequal("\r\n", "\n") == [ r"'\r\n' == '\n'", r"Strings contain only whitespace, escaping them using repr()", @@ -1212,8 +1224,8 @@ def test_reprcompare_whitespaces(): ] -def test_pytest_assertrepr_compare_integration(testdir): - testdir.makepyfile( +def test_pytest_assertrepr_compare_integration(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_hello(): x = set(range(100)) @@ -1222,7 +1234,7 @@ def test_hello(): assert x == y """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*def test_hello():*", @@ -1234,8 +1246,8 @@ def test_hello(): ) -def test_sequence_comparison_uses_repr(testdir): - testdir.makepyfile( +def test_sequence_comparison_uses_repr(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_hello(): x = set("hello x") @@ -1243,7 +1255,7 @@ def test_hello(): assert x == y """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*def test_hello():*", @@ -1256,19 +1268,20 @@ def test_hello(): ) -def test_assertrepr_loaded_per_dir(testdir): - testdir.makepyfile(test_base=["def test_base(): assert 1 == 2"]) - a = testdir.mkdir("a") - a_test = a.join("test_a.py") - a_test.write("def test_a(): assert 1 == 2") - a_conftest = a.join("conftest.py") - a_conftest.write('def pytest_assertrepr_compare(): return ["summary a"]') - b = testdir.mkdir("b") - b_test = b.join("test_b.py") - b_test.write("def test_b(): assert 1 == 2") - b_conftest = b.join("conftest.py") - b_conftest.write('def pytest_assertrepr_compare(): return ["summary b"]') - result = testdir.runpytest() +def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None: + pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"]) + a = pytester.mkdir("a") + a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2") + a.joinpath("conftest.py").write_text( + 'def pytest_assertrepr_compare(): return ["summary a"]' + ) + b = pytester.mkdir("b") + b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2") + b.joinpath("conftest.py").write_text( + 'def pytest_assertrepr_compare(): return ["summary b"]' + ) + + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*def test_base():*", @@ -1281,34 +1294,34 @@ def test_assertrepr_loaded_per_dir(testdir): ) -def test_assertion_options(testdir): - testdir.makepyfile( +def test_assertion_options(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_hello(): x = 3 assert x == 4 """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert "3 == 4" in result.stdout.str() - result = testdir.runpytest_subprocess("--assert=plain") + result = pytester.runpytest_subprocess("--assert=plain") result.stdout.no_fnmatch_line("*3 == 4*") -def test_triple_quoted_string_issue113(testdir): - testdir.makepyfile( +def test_triple_quoted_string_issue113(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_hello(): assert "" == ''' '''""" ) - result = testdir.runpytest("--fulltrace") + result = pytester.runpytest("--fulltrace") result.stdout.fnmatch_lines(["*1 failed*"]) result.stdout.no_fnmatch_line("*SyntaxError*") -def test_traceback_failure(testdir): - p1 = testdir.makepyfile( +def test_traceback_failure(pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def g(): return 2 @@ -1318,7 +1331,7 @@ def test_onefails(): f(3) """ ) - result = testdir.runpytest(p1, "--tb=long") + result = pytester.runpytest(p1, "--tb=long") result.stdout.fnmatch_lines( [ "*test_traceback_failure.py F*", @@ -1340,7 +1353,7 @@ def test_onefails(): ] ) - result = testdir.runpytest(p1) # "auto" + result = pytester.runpytest(p1) # "auto" result.stdout.fnmatch_lines( [ "*test_traceback_failure.py F*", @@ -1362,9 +1375,9 @@ def test_onefails(): ) -def test_exception_handling_no_traceback(testdir): +def test_exception_handling_no_traceback(pytester: Pytester) -> None: """Handle chain exceptions in tasks submitted by the multiprocess module (#1984).""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ from multiprocessing import Pool @@ -1380,8 +1393,8 @@ def test_multitask_job(): multitask_job() """ ) - testdir.syspathinsert() - result = testdir.runpytest(p1, "--tb=long") + pytester.syspathinsert() + result = pytester.runpytest(p1, "--tb=long") result.stdout.fnmatch_lines( [ "====* FAILURES *====", @@ -1419,27 +1432,27 @@ def test_multitask_job(): ), ], ) -def test_warn_missing(testdir, cmdline_args, warning_output): - testdir.makepyfile("") +def test_warn_missing(pytester: Pytester, cmdline_args, warning_output) -> None: + pytester.makepyfile("") - result = testdir.run(sys.executable, *cmdline_args) + result = pytester.run(sys.executable, *cmdline_args) result.stdout.fnmatch_lines(warning_output) -def test_recursion_source_decode(testdir): - testdir.makepyfile( +def test_recursion_source_decode(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_something(): pass """ ) - testdir.makeini( + pytester.makeini( """ [pytest] python_files = *.py """ ) - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines( """ @@ -1447,15 +1460,15 @@ def test_something(): ) -def test_AssertionError_message(testdir): - testdir.makepyfile( +def test_AssertionError_message(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_hello(): x,y = 1,2 assert 0, (x,y) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( """ *def test_hello* @@ -1465,15 +1478,15 @@ def test_hello(): ) -def test_diff_newline_at_end(testdir): - testdir.makepyfile( +def test_diff_newline_at_end(pytester: Pytester) -> None: + pytester.makepyfile( r""" def test_diff(): assert 'asdf' == 'asdf\n' """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( r""" *assert 'asdf' == 'asdf\n' @@ -1485,67 +1498,67 @@ def test_diff(): @pytest.mark.filterwarnings("default") -def test_assert_tuple_warning(testdir): +def test_assert_tuple_warning(pytester: Pytester) -> None: msg = "assertion is always true" - testdir.makepyfile( + pytester.makepyfile( """ def test_tuple(): assert(False, 'you shall not pass') """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines([f"*test_assert_tuple_warning.py:2:*{msg}*"]) # tuples with size != 2 should not trigger the warning - testdir.makepyfile( + pytester.makepyfile( """ def test_tuple(): assert () """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert msg not in result.stdout.str() -def test_assert_indirect_tuple_no_warning(testdir): - testdir.makepyfile( +def test_assert_indirect_tuple_no_warning(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_tuple(): tpl = ('foo', 'bar') assert tpl """ ) - result = testdir.runpytest() + result = pytester.runpytest() output = "\n".join(result.stdout.lines) assert "WR1" not in output -def test_assert_with_unicode(testdir): - testdir.makepyfile( +def test_assert_with_unicode(pytester: Pytester) -> None: + pytester.makepyfile( """\ def test_unicode(): assert '유니코드' == 'Unicode' """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*AssertionError*"]) -def test_raise_unprintable_assertion_error(testdir): - testdir.makepyfile( +def test_raise_unprintable_assertion_error(pytester: Pytester) -> None: + pytester.makepyfile( r""" def test_raise_assertion_error(): raise AssertionError('\xff') """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [r"> raise AssertionError('\xff')", "E AssertionError: *"] ) -def test_raise_assertion_error_raisin_repr(testdir): - testdir.makepyfile( +def test_raise_assertion_error_raisin_repr(pytester: Pytester) -> None: + pytester.makepyfile( """ class RaisingRepr(object): def __repr__(self): @@ -1554,14 +1567,14 @@ def test_raising_repr(): raise AssertionError(RaisingRepr()) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( ["E AssertionError: "] ) -def test_issue_1944(testdir): - testdir.makepyfile( +def test_issue_1944(pytester: Pytester) -> None: + pytester.makepyfile( """ def f(): return @@ -1569,7 +1582,7 @@ def f(): assert f() == 10 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 error*"]) assert ( "AttributeError: 'Module' object has no attribute '_obj'" @@ -1577,7 +1590,7 @@ def f(): ) -def test_exit_from_assertrepr_compare(monkeypatch): +def test_exit_from_assertrepr_compare(monkeypatch) -> None: def raise_exit(obj): outcomes.exit("Quitting debugger") @@ -1587,16 +1600,16 @@ def raise_exit(obj): callequal(1, 1) -def test_assertion_location_with_coverage(testdir): +def test_assertion_location_with_coverage(pytester: Pytester) -> None: """This used to report the wrong location when run with coverage (#5754).""" - p = testdir.makepyfile( + p = pytester.makepyfile( """ def test(): assert False, 1 assert False, 2 """ ) - result = testdir.runpytest(str(p)) + result = pytester.runpytest(str(p)) result.stdout.fnmatch_lines( [ "> assert False, 1", From 6cddeb8cb3779480ea8c57a62fdf109b1bfe2271 Mon Sep 17 00:00:00 2001 From: Simon K Date: Fri, 30 Oct 2020 19:13:06 +0000 Subject: [PATCH 740/823] #7938 - [Plugin: Stepwise][Enhancements] Refactoring, smarter registration & --sw-skip functionality (#7939) * adding --sw-skip shorthand for stepwise skip * be explicit rather than implicit with default args for stepwise * add constant for sw cache dir; only register plugin if necessary rather check check activity always; * use str format; remove unused args in hooks * assert cache upfront, allow stepwise to have a reference to the cache * type hinting lf, skip, move literal strings into module constants * convert parametrized option into a list * add a sessionfinish hook for stepwise to keep backwards behaviour the same * add changelog for #7938 * Improve performance of stepwise modifyitems & address PR feedback * add test for stepwise deselected based on performance enhancements * Apply suggestions from code review * delete from items, account for edge case where failed_index = 0 Co-authored-by: Bruno Oliveira --- changelog/7938.improvement.rst | 1 + src/_pytest/stepwise.py | 77 ++++++++++++++++------------------ testing/test_stepwise.py | 27 ++++++++---- 3 files changed, 58 insertions(+), 47 deletions(-) create mode 100644 changelog/7938.improvement.rst diff --git a/changelog/7938.improvement.rst b/changelog/7938.improvement.rst new file mode 100644 index 00000000000..ffe612d0da6 --- /dev/null +++ b/changelog/7938.improvement.rst @@ -0,0 +1 @@ +New ``--sw-skip`` argument which is a shorthand for ``--stepwise-skip``. diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 97eae18fd20..197577c790f 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,5 +1,6 @@ from typing import List from typing import Optional +from typing import TYPE_CHECKING import pytest from _pytest import nodes @@ -8,6 +9,11 @@ from _pytest.main import Session from _pytest.reports import TestReport +if TYPE_CHECKING: + from _pytest.cacheprovider import Cache + +STEPWISE_CACHE_DIR = "cache/stepwise" + def pytest_addoption(parser: Parser) -> None: group = parser.getgroup("general") @@ -15,12 +21,15 @@ def pytest_addoption(parser: Parser) -> None: "--sw", "--stepwise", action="store_true", + default=False, dest="stepwise", help="exit on test failure and continue from last failing test next time", ) group.addoption( + "--sw-skip", "--stepwise-skip", action="store_true", + default=False, dest="stepwise_skip", help="ignore the first failing test but stop on the next failing test", ) @@ -28,63 +37,56 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl def pytest_configure(config: Config) -> None: - config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") + # We should always have a cache as cache provider plugin uses tryfirst=True + if config.getoption("stepwise"): + config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") + + +def pytest_sessionfinish(session: Session) -> None: + if not session.config.getoption("stepwise"): + assert session.config.cache is not None + # Clear the list of failing tests if the plugin is not active. + session.config.cache.set(STEPWISE_CACHE_DIR, []) class StepwisePlugin: def __init__(self, config: Config) -> None: self.config = config - self.active = config.getvalue("stepwise") self.session: Optional[Session] = None self.report_status = "" - - if self.active: - assert config.cache is not None - self.lastfailed = config.cache.get("cache/stepwise", None) - self.skip = config.getvalue("stepwise_skip") + assert config.cache is not None + self.cache: Cache = config.cache + self.lastfailed: Optional[str] = self.cache.get(STEPWISE_CACHE_DIR, None) + self.skip: bool = config.getoption("stepwise_skip") def pytest_sessionstart(self, session: Session) -> None: self.session = session def pytest_collection_modifyitems( - self, session: Session, config: Config, items: List[nodes.Item] + self, config: Config, items: List[nodes.Item] ) -> None: - if not self.active: - return if not self.lastfailed: self.report_status = "no previously failed tests, not skipping." return - already_passed = [] - found = False - - # Make a list of all tests that have been run before the last failing one. - for item in items: + # check all item nodes until we find a match on last failed + failed_index = None + for index, item in enumerate(items): if item.nodeid == self.lastfailed: - found = True + failed_index = index break - else: - already_passed.append(item) # If the previously failed test was not found among the test items, # do not skip any tests. - if not found: + if failed_index is None: self.report_status = "previously failed test not found, not skipping." - already_passed = [] else: - self.report_status = "skipping {} already passed items.".format( - len(already_passed) - ) - - for item in already_passed: - items.remove(item) - - config.hook.pytest_deselected(items=already_passed) + self.report_status = f"skipping {failed_index} already passed items." + deselected = items[:failed_index] + del items[:failed_index] + config.hook.pytest_deselected(items=deselected) def pytest_runtest_logreport(self, report: TestReport) -> None: - if not self.active: - return - if report.failed: if self.skip: # Remove test from the failed ones (if it exists) and unset the skip option @@ -109,14 +111,9 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: self.lastfailed = None def pytest_report_collectionfinish(self) -> Optional[str]: - if self.active and self.config.getoption("verbose") >= 0 and self.report_status: - return "stepwise: %s" % self.report_status + if self.config.getoption("verbose") >= 0 and self.report_status: + return f"stepwise: {self.report_status}" return None - def pytest_sessionfinish(self, session: Session) -> None: - assert self.config.cache is not None - if self.active: - self.config.cache.set("cache/stepwise", self.lastfailed) - else: - # Clear the list of failing tests if the plugin is not active. - self.config.cache.set("cache/stepwise", []) + def pytest_sessionfinish(self) -> None: + self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index df66d798bbe..bf8d94f1d9e 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -93,6 +93,23 @@ def test_run_without_stepwise(stepwise_testdir): result.stdout.fnmatch_lines(["*test_success_after_fail PASSED*"]) +def test_stepwise_output_summary(testdir): + testdir.makepyfile( + """ + import pytest + @pytest.mark.parametrize("expected", [True, True, True, True, False]) + def test_data(expected): + assert expected + """ + ) + result = testdir.runpytest("-v", "--stepwise") + result.stdout.fnmatch_lines(["stepwise: no previously failed tests, not skipping."]) + result = testdir.runpytest("-v", "--stepwise") + result.stdout.fnmatch_lines( + ["stepwise: skipping 4 already passed items.", "*1 failed, 4 deselected*"] + ) + + def test_fail_and_continue_with_stepwise(stepwise_testdir): # Run the tests with a failing second test. result = stepwise_testdir.runpytest( @@ -117,14 +134,10 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): assert "test_success_after_fail PASSED" in stdout -def test_run_with_skip_option(stepwise_testdir): +@pytest.mark.parametrize("stepwise_skip", ["--stepwise-skip", "--sw-skip"]) +def test_run_with_skip_option(stepwise_testdir, stepwise_skip): result = stepwise_testdir.runpytest( - "-v", - "--strict-markers", - "--stepwise", - "--stepwise-skip", - "--fail", - "--fail-last", + "-v", "--strict-markers", "--stepwise", stepwise_skip, "--fail", "--fail-last", ) assert _strip_resource_warnings(result.stderr.lines) == [] From 5913cd20ec7c3f0a8305aa072110da5c896fddb8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 30 Oct 2020 21:15:48 +0200 Subject: [PATCH 741/823] assertion/util: remove unhelpful `type_fns` indirection It doesn't serve any purpose that I am able to discern. --- src/_pytest/assertion/util.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 08ff4eacd2b..93fa48b8e36 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -9,7 +9,6 @@ from typing import Mapping from typing import Optional from typing import Sequence -from typing import Tuple import _pytest._code from _pytest import outcomes @@ -179,8 +178,7 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: elif isdict(left) and isdict(right): explanation = _compare_eq_dict(left, right, verbose) elif type(left) == type(right) and (isdatacls(left) or isattrs(left)): - type_fn = (isdatacls, isattrs) - explanation = _compare_eq_cls(left, right, verbose, type_fn) + explanation = _compare_eq_cls(left, right, verbose) elif verbose > 0: explanation = _compare_eq_verbose(left, right) if isiterable(left) and isiterable(right): @@ -403,13 +401,7 @@ def _compare_eq_dict( return explanation -def _compare_eq_cls( - left: Any, - right: Any, - verbose: int, - type_fns: Tuple[Callable[[Any], bool], Callable[[Any], bool]], -) -> List[str]: - isdatacls, isattrs = type_fns +def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: if isdatacls(left): all_fields = left.__dataclass_fields__ fields_to_check = [field for field, info in all_fields.items() if info.compare] From c58abf7ad18c56ca516c50780cd726eff3c4f44c Mon Sep 17 00:00:00 2001 From: symonk Date: Fri, 30 Oct 2020 19:21:42 +0000 Subject: [PATCH 742/823] #7942 refactor stepwise tests to utilize pytester --- testing/test_stepwise.py | 79 +++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index bf8d94f1d9e..ff2ec16b707 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -1,11 +1,13 @@ import pytest +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Pytester @pytest.fixture -def stepwise_testdir(testdir): +def stepwise_pytester(pytester: Pytester) -> Pytester: # Rather than having to modify our testfile between tests, we introduce # a flag for whether or not the second test should fail. - testdir.makeconftest( + pytester.makeconftest( """ def pytest_addoption(parser): group = parser.getgroup('general') @@ -15,7 +17,7 @@ def pytest_addoption(parser): ) # Create a simple test suite. - testdir.makepyfile( + pytester.makepyfile( test_a=""" def test_success_before_fail(): assert 1 @@ -34,7 +36,7 @@ def test_success_after_last_fail(): """ ) - testdir.makepyfile( + pytester.makepyfile( test_b=""" def test_success(): assert 1 @@ -42,19 +44,19 @@ def test_success(): ) # customize cache directory so we don't use the tox's cache directory, which makes tests in this module flaky - testdir.makeini( + pytester.makeini( """ [pytest] cache_dir = .cache """ ) - return testdir + return pytester @pytest.fixture -def error_testdir(testdir): - testdir.makepyfile( +def error_pytester(pytester: Pytester) -> Pytester: + pytester.makepyfile( test_a=""" def test_error(nonexisting_fixture): assert 1 @@ -64,15 +66,15 @@ def test_success_after_fail(): """ ) - return testdir + return pytester @pytest.fixture -def broken_testdir(testdir): - testdir.makepyfile( +def broken_pytester(pytester: Pytester) -> Pytester: + pytester.makepyfile( working_testfile="def test_proper(): assert 1", broken_testfile="foobar" ) - return testdir + return pytester def _strip_resource_warnings(lines): @@ -85,16 +87,15 @@ def _strip_resource_warnings(lines): ] -def test_run_without_stepwise(stepwise_testdir): - result = stepwise_testdir.runpytest("-v", "--strict-markers", "--fail") - +def test_run_without_stepwise(stepwise_pytester: Pytester) -> None: + result = stepwise_pytester.runpytest("-v", "--strict-markers", "--fail") result.stdout.fnmatch_lines(["*test_success_before_fail PASSED*"]) result.stdout.fnmatch_lines(["*test_fail_on_flag FAILED*"]) result.stdout.fnmatch_lines(["*test_success_after_fail PASSED*"]) -def test_stepwise_output_summary(testdir): - testdir.makepyfile( +def test_stepwise_output_summary(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.parametrize("expected", [True, True, True, True, False]) @@ -102,17 +103,17 @@ def test_data(expected): assert expected """ ) - result = testdir.runpytest("-v", "--stepwise") + result = pytester.runpytest("-v", "--stepwise") result.stdout.fnmatch_lines(["stepwise: no previously failed tests, not skipping."]) - result = testdir.runpytest("-v", "--stepwise") + result = pytester.runpytest("-v", "--stepwise") result.stdout.fnmatch_lines( ["stepwise: skipping 4 already passed items.", "*1 failed, 4 deselected*"] ) -def test_fail_and_continue_with_stepwise(stepwise_testdir): +def test_fail_and_continue_with_stepwise(stepwise_pytester: Pytester) -> None: # Run the tests with a failing second test. - result = stepwise_testdir.runpytest( + result = stepwise_pytester.runpytest( "-v", "--strict-markers", "--stepwise", "--fail" ) assert _strip_resource_warnings(result.stderr.lines) == [] @@ -124,7 +125,7 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): assert "test_success_after_fail" not in stdout # "Fix" the test that failed in the last run and run it again. - result = stepwise_testdir.runpytest("-v", "--strict-markers", "--stepwise") + result = stepwise_pytester.runpytest("-v", "--strict-markers", "--stepwise") assert _strip_resource_warnings(result.stderr.lines) == [] stdout = result.stdout.str() @@ -135,8 +136,8 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): @pytest.mark.parametrize("stepwise_skip", ["--stepwise-skip", "--sw-skip"]) -def test_run_with_skip_option(stepwise_testdir, stepwise_skip): - result = stepwise_testdir.runpytest( +def test_run_with_skip_option(stepwise_pytester: Pytester, stepwise_skip: str) -> None: + result = stepwise_pytester.runpytest( "-v", "--strict-markers", "--stepwise", stepwise_skip, "--fail", "--fail-last", ) assert _strip_resource_warnings(result.stderr.lines) == [] @@ -149,8 +150,8 @@ def test_run_with_skip_option(stepwise_testdir, stepwise_skip): assert "test_success_after_last_fail" not in stdout -def test_fail_on_errors(error_testdir): - result = error_testdir.runpytest("-v", "--strict-markers", "--stepwise") +def test_fail_on_errors(error_pytester: Pytester) -> None: + result = error_pytester.runpytest("-v", "--strict-markers", "--stepwise") assert _strip_resource_warnings(result.stderr.lines) == [] stdout = result.stdout.str() @@ -159,8 +160,8 @@ def test_fail_on_errors(error_testdir): assert "test_success_after_fail" not in stdout -def test_change_testfile(stepwise_testdir): - result = stepwise_testdir.runpytest( +def test_change_testfile(stepwise_pytester: Pytester) -> None: + result = stepwise_pytester.runpytest( "-v", "--strict-markers", "--stepwise", "--fail", "test_a.py" ) assert _strip_resource_warnings(result.stderr.lines) == [] @@ -170,7 +171,7 @@ def test_change_testfile(stepwise_testdir): # Make sure the second test run starts from the beginning, since the # test to continue from does not exist in testfile_b. - result = stepwise_testdir.runpytest( + result = stepwise_pytester.runpytest( "-v", "--strict-markers", "--stepwise", "test_b.py" ) assert _strip_resource_warnings(result.stderr.lines) == [] @@ -180,17 +181,19 @@ def test_change_testfile(stepwise_testdir): @pytest.mark.parametrize("broken_first", [True, False]) -def test_stop_on_collection_errors(broken_testdir, broken_first): +def test_stop_on_collection_errors( + broken_pytester: Pytester, broken_first: bool +) -> None: """Stop during collection errors. Broken test first or broken test last actually surfaced a bug (#5444), so we test both situations.""" files = ["working_testfile.py", "broken_testfile.py"] if broken_first: files.reverse() - result = broken_testdir.runpytest("-v", "--strict-markers", "--stepwise", *files) + result = broken_pytester.runpytest("-v", "--strict-markers", "--stepwise", *files) result.stdout.fnmatch_lines("*error during collection*") -def test_xfail_handling(testdir, monkeypatch): +def test_xfail_handling(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: """Ensure normal xfail is ignored, and strict xfail interrupts the session in sw mode (#5547) @@ -207,8 +210,8 @@ def test_b(): assert {assert_value} def test_c(): pass def test_d(): pass """ - testdir.makepyfile(contents.format(assert_value="0", strict="False")) - result = testdir.runpytest("--sw", "-v") + pytester.makepyfile(contents.format(assert_value="0", strict="False")) + result = pytester.runpytest("--sw", "-v") result.stdout.fnmatch_lines( [ "*::test_a PASSED *", @@ -219,8 +222,8 @@ def test_d(): pass ] ) - testdir.makepyfile(contents.format(assert_value="1", strict="True")) - result = testdir.runpytest("--sw", "-v") + pytester.makepyfile(contents.format(assert_value="1", strict="True")) + result = pytester.runpytest("--sw", "-v") result.stdout.fnmatch_lines( [ "*::test_a PASSED *", @@ -230,8 +233,8 @@ def test_d(): pass ] ) - testdir.makepyfile(contents.format(assert_value="0", strict="True")) - result = testdir.runpytest("--sw", "-v") + pytester.makepyfile(contents.format(assert_value="0", strict="True")) + result = pytester.runpytest("--sw", "-v") result.stdout.fnmatch_lines( [ "*::test_b XFAIL *", From aa843746a4c4fc92aeabf7bf9c6822a7886c4c6c Mon Sep 17 00:00:00 2001 From: Christine Mecklenborg Date: Fri, 30 Oct 2020 15:12:40 -0500 Subject: [PATCH 743/823] Migrate test_error_diffs.py from testdir to pytester (#7971) --- testing/test_error_diffs.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index 2857df83236..1668e929ab4 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -7,6 +7,7 @@ import sys import pytest +from _pytest.pytester import Pytester TESTCASES = [ @@ -274,9 +275,9 @@ def test_this(): @pytest.mark.parametrize("code, expected", TESTCASES) -def test_error_diff(code, expected, testdir): - expected = [line.lstrip() for line in expected.splitlines()] - p = testdir.makepyfile(code) - result = testdir.runpytest(p, "-vv") - result.stdout.fnmatch_lines(expected) +def test_error_diff(code: str, expected: str, pytester: Pytester) -> None: + expected_lines = [line.lstrip() for line in expected.splitlines()] + p = pytester.makepyfile(code) + result = pytester.runpytest(p, "-vv") + result.stdout.fnmatch_lines(expected_lines) assert result.ret == 1 From 3c7eb5a398c325da20c82142eccdb5bb8c3d0691 Mon Sep 17 00:00:00 2001 From: crricks <72474049+crricks@users.noreply.github.com> Date: Fri, 30 Oct 2020 14:34:05 -0600 Subject: [PATCH 744/823] migrated test_nodes.py from testdir to pytester #7492. (#7969) --- testing/test_nodes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/testing/test_nodes.py b/testing/test_nodes.py index b72a94ebeb0..627be930177 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -4,7 +4,7 @@ import pytest from _pytest import nodes -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester @pytest.mark.parametrize( @@ -35,8 +35,8 @@ def test_node_from_parent_disallowed_arguments() -> None: nodes.Node.from_parent(None, config=None) # type: ignore[arg-type] -def test_std_warn_not_pytestwarning(testdir: Testdir) -> None: - items = testdir.getitems( +def test_std_warn_not_pytestwarning(pytester: Pytester) -> None: + items = pytester.getitems( """ def test(): pass @@ -66,12 +66,12 @@ class FakeSession2: assert nodes._check_initialpaths_for_relpath(FakeSession2, outside) is None -def test_failure_with_changed_cwd(testdir): +def test_failure_with_changed_cwd(pytester: Pytester) -> None: """ Test failure lines should use absolute paths if cwd has changed since invocation, so the path is correct (#6428). """ - p = testdir.makepyfile( + p = pytester.makepyfile( """ import os import pytest @@ -89,5 +89,5 @@ def test_show_wrong_path(private_dir): assert False """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines([str(p) + ":*: AssertionError", "*1 failed in *"]) From a7e38c5c61928033a2dc1915cbee8caa8544a4d0 Mon Sep 17 00:00:00 2001 From: Ariel Pillemer <63328798+pillemer@users.noreply.github.com> Date: Sat, 31 Oct 2020 20:08:11 +0930 Subject: [PATCH 745/823] pytest-dev#7942 test_runner_xunit.py (#7964) --- AUTHORS | 1 + testing/test_runner_xunit.py | 57 ++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/AUTHORS b/AUTHORS index 35d220e0044..f7e811544c9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -32,6 +32,7 @@ Anthony Sottile Anton Lodder Antony Lee Arel Cordero +Ariel Pillemer Armin Rigo Aron Coyle Aron Curzon diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index ef65a24cd79..e90d761f633 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -2,10 +2,11 @@ from typing import List import pytest +from _pytest.pytester import Pytester -def test_module_and_function_setup(testdir): - reprec = testdir.inline_runsource( +def test_module_and_function_setup(pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ modlevel = [] def setup_module(module): @@ -37,8 +38,8 @@ def test_module(self): assert rep.passed -def test_module_setup_failure_no_teardown(testdir): - reprec = testdir.inline_runsource( +def test_module_setup_failure_no_teardown(pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ values = [] def setup_module(module): @@ -57,8 +58,8 @@ def teardown_module(module): assert calls[0].item.module.values == [1] -def test_setup_function_failure_no_teardown(testdir): - reprec = testdir.inline_runsource( +def test_setup_function_failure_no_teardown(pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ modlevel = [] def setup_function(function): @@ -76,8 +77,8 @@ def test_func(): assert calls[0].item.module.modlevel == [1] -def test_class_setup(testdir): - reprec = testdir.inline_runsource( +def test_class_setup(pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ class TestSimpleClassSetup(object): clslevel = [] @@ -102,8 +103,8 @@ def test_cleanup(): reprec.assertoutcome(passed=1 + 2 + 1) -def test_class_setup_failure_no_teardown(testdir): - reprec = testdir.inline_runsource( +def test_class_setup_failure_no_teardown(pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ class TestSimpleClassSetup(object): clslevel = [] @@ -123,8 +124,8 @@ def test_cleanup(): reprec.assertoutcome(failed=1, passed=1) -def test_method_setup(testdir): - reprec = testdir.inline_runsource( +def test_method_setup(pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ class TestSetupMethod(object): def setup_method(self, meth): @@ -142,8 +143,8 @@ def test_other(self): reprec.assertoutcome(passed=2) -def test_method_setup_failure_no_teardown(testdir): - reprec = testdir.inline_runsource( +def test_method_setup_failure_no_teardown(pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ class TestMethodSetup(object): clslevel = [] @@ -164,8 +165,8 @@ def test_cleanup(): reprec.assertoutcome(failed=1, passed=1) -def test_method_setup_uses_fresh_instances(testdir): - reprec = testdir.inline_runsource( +def test_method_setup_uses_fresh_instances(pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ class TestSelfState1(object): memory = [] @@ -179,8 +180,8 @@ def test_afterhello(self): reprec.assertoutcome(passed=2, failed=0) -def test_setup_that_skips_calledagain(testdir): - p = testdir.makepyfile( +def test_setup_that_skips_calledagain(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest def setup_module(mod): @@ -191,12 +192,12 @@ def test_function2(): pass """ ) - reprec = testdir.inline_run(p) + reprec = pytester.inline_run(p) reprec.assertoutcome(skipped=2) -def test_setup_fails_again_on_all_tests(testdir): - p = testdir.makepyfile( +def test_setup_fails_again_on_all_tests(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest def setup_module(mod): @@ -207,12 +208,12 @@ def test_function2(): pass """ ) - reprec = testdir.inline_run(p) + reprec = pytester.inline_run(p) reprec.assertoutcome(failed=2) -def test_setup_funcarg_setup_when_outer_scope_fails(testdir): - p = testdir.makepyfile( +def test_setup_funcarg_setup_when_outer_scope_fails(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest def setup_module(mod): @@ -226,7 +227,7 @@ def test_function2(hello): pass """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines( [ "*function1*", @@ -241,7 +242,7 @@ def test_function2(hello): @pytest.mark.parametrize("arg", ["", "arg"]) def test_setup_teardown_function_level_with_optional_argument( - testdir, monkeypatch, arg: str, + pytester: Pytester, monkeypatch, arg: str, ) -> None: """Parameter to setup/teardown xunit-style functions parameter is now optional (#1728).""" import sys @@ -250,7 +251,7 @@ def test_setup_teardown_function_level_with_optional_argument( monkeypatch.setattr( sys, "trace_setups_teardowns", trace_setups_teardowns, raising=False ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ import pytest import sys @@ -276,7 +277,7 @@ def test_method_2(self): pass arg=arg ) ) - result = testdir.inline_run(p) + result = pytester.inline_run(p) result.assertoutcome(passed=4) expected = [ From a1df458e854afe030d8dc65b7beac783bbd255a4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 27 Oct 2020 10:18:23 +0200 Subject: [PATCH 746/823] code: use properties for derived attributes, use slots Make the objects more light weight. Remove unused properties. --- src/_pytest/_code/code.py | 46 +++++++++++++++++++++++++++--------- testing/code/test_excinfo.py | 1 - 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 2371b44d938..430e4524255 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -54,14 +54,13 @@ class Code: """Wrapper around Python code objects.""" + __slots__ = ("raw",) + def __init__(self, rawcode) -> None: if not hasattr(rawcode, "co_filename"): rawcode = getrawcode(rawcode) if not isinstance(rawcode, CodeType): raise TypeError(f"not a code object: {rawcode!r}") - self.filename = rawcode.co_filename - self.firstlineno = rawcode.co_firstlineno - 1 - self.name = rawcode.co_name self.raw = rawcode def __eq__(self, other): @@ -70,6 +69,14 @@ def __eq__(self, other): # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore + @property + def firstlineno(self) -> int: + return self.raw.co_firstlineno - 1 + + @property + def name(self) -> str: + return self.raw.co_name + @property def path(self) -> Union[py.path.local, str]: """Return a path object pointing to source code, or an ``str`` in @@ -117,12 +124,26 @@ class Frame: """Wrapper around a Python frame holding f_locals and f_globals in which expressions can be evaluated.""" + __slots__ = ("raw",) + def __init__(self, frame: FrameType) -> None: - self.lineno = frame.f_lineno - 1 - self.f_globals = frame.f_globals - self.f_locals = frame.f_locals self.raw = frame - self.code = Code(frame.f_code) + + @property + def lineno(self) -> int: + return self.raw.f_lineno - 1 + + @property + def f_globals(self) -> Dict[str, Any]: + return self.raw.f_globals + + @property + def f_locals(self) -> Dict[str, Any]: + return self.raw.f_locals + + @property + def code(self) -> Code: + return Code(self.raw.f_code) @property def statement(self) -> "Source": @@ -164,17 +185,20 @@ def getargs(self, var: bool = False): class TracebackEntry: """A single entry in a Traceback.""" - _repr_style: Optional['Literal["short", "long"]'] = None - exprinfo = None + __slots__ = ("_rawentry", "_excinfo", "_repr_style") def __init__( self, rawentry: TracebackType, excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, ) -> None: - self._excinfo = excinfo self._rawentry = rawentry - self.lineno = rawentry.tb_lineno - 1 + self._excinfo = excinfo + self._repr_style: Optional['Literal["short", "long"]'] = None + + @property + def lineno(self) -> int: + return self._rawentry.tb_lineno - 1 def set_repr_style(self, mode: "Literal['short', 'long']") -> None: assert mode in ("short", "long") diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index a55da643068..a43704ff034 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -747,7 +747,6 @@ def entry(): from _pytest._code.code import Code monkeypatch.setattr(Code, "path", "bogus") - excinfo.traceback[0].frame.code.path = "bogus" # type: ignore[misc] p = FormattedExcinfo(style="short") reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) lines = reprtb.lines From 6506f016acf77415b7d682bf15cac865ab39273f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 27 Oct 2020 16:11:39 +0200 Subject: [PATCH 747/823] testing/test_source: use unqualified imports --- testing/code/test_source.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index e259e04cfef..fa2136ef1b0 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -13,10 +13,11 @@ import py.path -import _pytest._code import pytest -from _pytest._code import getfslineno +from _pytest._code import Code +from _pytest._code import Frame from _pytest._code import Source +from _pytest._code import getfslineno def test_source_str_function() -> None: @@ -35,7 +36,7 @@ def test_source_str_function() -> None: def test_source_from_function() -> None: - source = _pytest._code.Source(test_source_str_function) + source = Source(test_source_str_function) assert str(source).startswith("def test_source_str_function() -> None:") @@ -44,13 +45,13 @@ class TestClass: def test_method(self): pass - source = _pytest._code.Source(TestClass().test_method) + source = Source(TestClass().test_method) assert source.lines == ["def test_method(self):", " pass"] def test_source_from_lines() -> None: lines = ["a \n", "b\n", "c"] - source = _pytest._code.Source(lines) + source = Source(lines) assert source.lines == ["a ", "b", "c"] @@ -58,7 +59,7 @@ def test_source_from_inner_function() -> None: def f(): raise NotImplementedError() - source = _pytest._code.Source(f) + source = Source(f) assert str(source).startswith("def f():") @@ -220,7 +221,7 @@ def test_getstartingblock_singleline() -> None: class A: def __init__(self, *args) -> None: frame = sys._getframe(1) - self.source = _pytest._code.Frame(frame).statement + self.source = Frame(frame).statement x = A("x", "y") @@ -250,8 +251,8 @@ def f(): def g(): pass # pragma: no cover - f_source = _pytest._code.Source(f) - g_source = _pytest._code.Source(g) + f_source = Source(f) + g_source = Source(g) assert str(f_source).strip() == "def f():\n raise NotImplementedError()" assert str(g_source).strip() == "def g():\n pass # pragma: no cover" @@ -268,7 +269,7 @@ def f(): pass """ ''' - assert str(_pytest._code.Source(f)) == expected.rstrip() + assert str(Source(f)) == expected.rstrip() def test_deindent() -> None: @@ -288,7 +289,7 @@ def g(): def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot) -> None: # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. - source = _pytest._code.Source( + source = Source( """ class A(object): def method(self): @@ -297,7 +298,7 @@ def method(self): ) path = tmpdir.join("a.py") path.write(source) - s2 = _pytest._code.Source(tmpdir.join("a.py").pyimport().A) + s2 = Source(tmpdir.join("a.py").pyimport().A) assert str(source).strip() == str(s2).strip() @@ -386,26 +387,26 @@ def test_code_of_object_instance_with_call() -> None: class A: pass - pytest.raises(TypeError, lambda: _pytest._code.Source(A())) + pytest.raises(TypeError, lambda: Source(A())) class WithCall: def __call__(self) -> None: pass - code = _pytest._code.Code(WithCall()) + code = Code(WithCall()) assert "pass" in str(code.source()) class Hello: def __call__(self) -> None: pass - pytest.raises(TypeError, lambda: _pytest._code.Code(Hello)) + pytest.raises(TypeError, lambda: Code(Hello)) def getstatement(lineno: int, source) -> Source: from _pytest._code.source import getstatementrange_ast - src = _pytest._code.Source(source) + src = Source(source) ast, start, end = getstatementrange_ast(lineno, src) return src[start:end] @@ -637,7 +638,7 @@ def test_getstartingblock_multiline() -> None: class A: def __init__(self, *args): frame = sys._getframe(1) - self.source = _pytest._code.Frame(frame).statement + self.source = Frame(frame).statement # fmt: off x = A('x', From 531416cc5a85e7e90c03ad75962fa5caf92fcf36 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 27 Oct 2020 16:07:03 +0200 Subject: [PATCH 748/823] code: simplify Code construction --- src/_pytest/_code/code.py | 14 +++++++------- src/_pytest/_code/source.py | 26 ++++++++++++++------------ src/_pytest/python.py | 2 +- testing/code/test_code.py | 19 ++++++++++--------- testing/code/test_excinfo.py | 6 +++--- testing/code/test_source.py | 16 ++++------------ testing/test_assertrewrite.py | 2 +- 7 files changed, 40 insertions(+), 45 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 430e4524255..423069330a5 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -56,12 +56,12 @@ class Code: __slots__ = ("raw",) - def __init__(self, rawcode) -> None: - if not hasattr(rawcode, "co_filename"): - rawcode = getrawcode(rawcode) - if not isinstance(rawcode, CodeType): - raise TypeError(f"not a code object: {rawcode!r}") - self.raw = rawcode + def __init__(self, obj: CodeType) -> None: + self.raw = obj + + @classmethod + def from_function(cls, obj: object) -> "Code": + return cls(getrawcode(obj)) def __eq__(self, other): return self.raw == other.raw @@ -1196,7 +1196,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: obj = obj.place_as # type: ignore[attr-defined] try: - code = Code(obj) + code = Code.from_function(obj) except TypeError: try: fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type] diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index c63a42360c6..6f54057c0a9 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -2,6 +2,7 @@ import inspect import textwrap import tokenize +import types import warnings from bisect import bisect_right from typing import Iterable @@ -29,8 +30,11 @@ def __init__(self, obj: object = None) -> None: elif isinstance(obj, str): self.lines = deindent(obj.split("\n")) else: - rawcode = getrawcode(obj) - src = inspect.getsource(rawcode) + try: + rawcode = getrawcode(obj) + src = inspect.getsource(rawcode) + except TypeError: + src = inspect.getsource(obj) # type: ignore[arg-type] self.lines = deindent(src.split("\n")) def __eq__(self, other: object) -> bool: @@ -122,19 +126,17 @@ def findsource(obj) -> Tuple[Optional[Source], int]: return source, lineno -def getrawcode(obj, trycall: bool = True): +def getrawcode(obj: object, trycall: bool = True) -> types.CodeType: """Return code object for given function.""" try: - return obj.__code__ + return obj.__code__ # type: ignore[attr-defined,no-any-return] except AttributeError: - obj = getattr(obj, "f_code", obj) - obj = getattr(obj, "__code__", obj) - if trycall and not hasattr(obj, "co_firstlineno"): - if hasattr(obj, "__call__") and not inspect.isclass(obj): - x = getrawcode(obj.__call__, trycall=False) - if hasattr(x, "co_firstlineno"): - return x - return obj + pass + if trycall: + call = getattr(obj, "__call__", None) + if call and not isinstance(obj, type): + return getrawcode(call, trycall=False) + raise TypeError(f"could not get code object for {obj!r}") def deindent(lines: Iterable[str]) -> List[str]: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 35797cc0762..e477b8b4501 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1647,7 +1647,7 @@ def setup(self) -> None: def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): - code = _pytest._code.Code(get_real_func(self.obj)) + code = _pytest._code.Code.from_function(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) diff --git a/testing/code/test_code.py b/testing/code/test_code.py index bae86be347f..33809528a06 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -28,11 +28,12 @@ def test_code_gives_back_name_for_not_existing_file() -> None: assert code.fullsource is None -def test_code_with_class() -> None: +def test_code_from_function_with_class() -> None: class A: pass - pytest.raises(TypeError, Code, A) + with pytest.raises(TypeError): + Code.from_function(A) def x() -> None: @@ -40,13 +41,13 @@ def x() -> None: def test_code_fullsource() -> None: - code = Code(x) + code = Code.from_function(x) full = code.fullsource assert "test_code_fullsource()" in str(full) def test_code_source() -> None: - code = Code(x) + code = Code.from_function(x) src = code.source() expected = """def x() -> None: raise NotImplementedError()""" @@ -73,7 +74,7 @@ def func() -> FrameType: def test_code_from_func() -> None: - co = Code(test_frame_getsourcelineno_myself) + co = Code.from_function(test_frame_getsourcelineno_myself) assert co.firstlineno assert co.path @@ -92,25 +93,25 @@ def test_code_getargs() -> None: def f1(x): raise NotImplementedError() - c1 = Code(f1) + c1 = Code.from_function(f1) assert c1.getargs(var=True) == ("x",) def f2(x, *y): raise NotImplementedError() - c2 = Code(f2) + c2 = Code.from_function(f2) assert c2.getargs(var=True) == ("x", "y") def f3(x, **z): raise NotImplementedError() - c3 = Code(f3) + c3 = Code.from_function(f3) assert c3.getargs(var=True) == ("x", "z") def f4(x, *y, **z): raise NotImplementedError() - c4 = Code(f4) + c4 = Code.from_function(f4) assert c4.getargs(var=True) == ("x", "y", "z") diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index a43704ff034..5b9e3eda529 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -147,7 +147,7 @@ def xyz(): ] def test_traceback_cut(self): - co = _pytest._code.Code(f) + co = _pytest._code.Code.from_function(f) path, firstlineno = co.path, co.firstlineno traceback = self.excinfo.traceback newtraceback = traceback.cut(path=path, firstlineno=firstlineno) @@ -290,7 +290,7 @@ def f(): excinfo = pytest.raises(ValueError, f) tb = excinfo.traceback entry = tb.getcrashentry() - co = _pytest._code.Code(h) + co = _pytest._code.Code.from_function(h) assert entry.frame.code.path == co.path assert entry.lineno == co.firstlineno + 1 assert entry.frame.code.name == "h" @@ -307,7 +307,7 @@ def f(): excinfo = pytest.raises(ValueError, f) tb = excinfo.traceback entry = tb.getcrashentry() - co = _pytest._code.Code(g) + co = _pytest._code.Code.from_function(g) assert entry.frame.code.path == co.path assert entry.lineno == co.firstlineno + 2 assert entry.frame.code.name == "g" diff --git a/testing/code/test_source.py b/testing/code/test_source.py index fa2136ef1b0..04d0ea9323d 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -16,8 +16,8 @@ import pytest from _pytest._code import Code from _pytest._code import Frame -from _pytest._code import Source from _pytest._code import getfslineno +from _pytest._code import Source def test_source_str_function() -> None: @@ -291,7 +291,7 @@ def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot) -> None: # does not return the "x = 1" last line. source = Source( """ - class A(object): + class A: def method(self): x = 1 """ @@ -374,14 +374,6 @@ class B: B.__name__ = B.__qualname__ = "B2" assert getfslineno(B)[1] == -1 - co = compile("...", "", "eval") - assert co.co_filename == "" - - if hasattr(sys, "pypy_version_info"): - assert getfslineno(co) == ("", -1) - else: - assert getfslineno(co) == ("", 0) - def test_code_of_object_instance_with_call() -> None: class A: @@ -393,14 +385,14 @@ class WithCall: def __call__(self) -> None: pass - code = Code(WithCall()) + code = Code.from_function(WithCall()) assert "pass" in str(code.source()) class Hello: def __call__(self) -> None: pass - pytest.raises(TypeError, lambda: Code(Hello)) + pytest.raises(TypeError, lambda: Code.from_function(Hello)) def getstatement(lineno: int, source) -> Source: diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 58a31ab8d24..09383cafee6 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -42,7 +42,7 @@ def getmsg( f, extra_ns: Optional[Mapping[str, object]] = None, *, must_pass: bool = False ) -> Optional[str]: """Rewrite the assertions in f, run it, and get the failure message.""" - src = "\n".join(_pytest._code.Code(f).source().lines) + src = "\n".join(_pytest._code.Code.from_function(f).source().lines) mod = rewrite(src) code = compile(mod, "", "exec") ns: Dict[str, object] = {} From 0c7233032f000ddf9d08f9d311fb6c850f4d0237 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 31 Oct 2020 08:36:26 -0300 Subject: [PATCH 749/823] Manually add the remaining 4.6.x release notes to the changelog Fix #7967 --- doc/en/changelog.rst | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 8897ece8cb8..3f14921a80a 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -1873,6 +1873,44 @@ Improved Documentation - `#5416 `_: Fix PytestUnknownMarkWarning in run/skip example. +pytest 4.6.11 (2020-06-04) +========================== + +Bug Fixes +--------- + +- `#6334 `_: Fix summary entries appearing twice when ``f/F`` and ``s/S`` report chars were used at the same time in the ``-r`` command-line option (for example ``-rFf``). + + The upper case variants were never documented and the preferred form should be the lower case. + + +- `#7310 `_: Fix ``UnboundLocalError: local variable 'letter' referenced before + assignment`` in ``_pytest.terminal.pytest_report_teststatus()`` + when plugins return report objects in an unconventional state. + + This was making ``pytest_report_teststatus()`` skip + entering if-block branches that declare the ``letter`` variable. + + The fix was to set the initial value of the ``letter`` before + the if-block cascade so that it always has a value. + + +pytest 4.6.10 (2020-05-08) +========================== + +Features +-------- + +- `#6870 `_: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. + + Remark: while this is technically a new feature and according to our `policy `_ it should not have been backported, we have opened an exception in this particular case because it fixes a serious interaction with ``pytest-xdist``, so it can also be considered a bugfix. + +Trivial/Internal Changes +------------------------ + +- `#6404 `_: Remove usage of ``parser`` module, deprecated in Python 3.9. + + pytest 4.6.9 (2020-01-04) ========================= From 569c091769d6739d262ee796e71f5f70dc81bf19 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 31 Oct 2020 08:45:34 -0300 Subject: [PATCH 750/823] Add FunctionDefinition to the reference docs Fix #7968 --- doc/en/reference.rst | 7 +++++++ src/_pytest/python.py | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index b243a52bd59..c04b8da0b1b 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -840,6 +840,13 @@ Function :members: :show-inheritance: +FunctionDefinition +~~~~~~~~~~~~~~~~~~ + +.. autoclass:: _pytest.python.FunctionDefinition() + :members: + :show-inheritance: + Item ~~~~ diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 35797cc0762..9e988032dde 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -960,6 +960,7 @@ def __init__( cls=None, module=None, ) -> None: + #: Access to the underlying :class:`_pytest.python.FunctionDefinition`. self.definition = definition #: Access to the :class:`_pytest.config.Config` object for the test session. @@ -1677,10 +1678,12 @@ def repr_failure( # type: ignore[override] class FunctionDefinition(Function): - """Internal hack until we get actual definition nodes instead of the - crappy metafunc hack.""" + """ + This class is a step 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 used") + raise RuntimeError("function definitions are not supposed to be run as tests") setup = runtest From 6cdae8ed40e329d82c6ae96dbb3eeff4be5ef5f4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 31 Oct 2020 13:55:02 +0200 Subject: [PATCH 751/823] pathlib: fix symlinked directories not followed during collection --- changelog/7981.bugfix.rst | 1 + src/_pytest/pathlib.py | 2 +- testing/test_collection.py | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 changelog/7981.bugfix.rst diff --git a/changelog/7981.bugfix.rst b/changelog/7981.bugfix.rst new file mode 100644 index 00000000000..0a254b5d49d --- /dev/null +++ b/changelog/7981.bugfix.rst @@ -0,0 +1 @@ +Fixed symlinked directories not being followed during collection. Regressed in pytest 6.1.0. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index f0bdb1481bb..b96cba06982 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -558,7 +558,7 @@ def visit( entries = sorted(os.scandir(path), key=lambda entry: entry.name) yield from entries for entry in entries: - if entry.is_dir(follow_symlinks=False) and recurse(entry): + if entry.is_dir() and recurse(entry): yield from visit(entry.path, recurse) diff --git a/testing/test_collection.py b/testing/test_collection.py index 841aa358b96..4a7a3e620c5 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -9,6 +9,7 @@ from _pytest.main import _in_venv from _pytest.main import Session from _pytest.pathlib import symlink_or_skip +from _pytest.pytester import Pytester from _pytest.pytester import Testdir @@ -1178,6 +1179,15 @@ def test_nodeid(request): assert result.ret == 0 +def test_collect_symlink_dir(pytester: Pytester) -> None: + """A symlinked directory is collected.""" + dir = pytester.mkdir("dir") + dir.joinpath("test_it.py").write_text("def test_it(): pass", "utf-8") + pytester.path.joinpath("symlink_dir").symlink_to(dir) + result = pytester.runpytest() + result.assert_outcomes(passed=2) + + def test_collectignore_via_conftest(testdir): """collect_ignore in parent conftest skips importing child (issue #4592).""" tests = testdir.mkpydir("tests") From 9a0f4e57ee6ec0602e2f5e6e53920bb1d985e316 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Tue, 28 Jul 2020 12:24:24 +0000 Subject: [PATCH 752/823] Add support to display field names in namedtuple diffs. --- AUTHORS | 1 + changelog/7527.improvement.rst | 1 + src/_pytest/assertion/util.py | 20 ++++++++++++++--- testing/test_assertion.py | 39 ++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 changelog/7527.improvement.rst diff --git a/AUTHORS b/AUTHORS index 35d220e0044..30ea946b0a6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -155,6 +155,7 @@ Justyna Janczyszyn Kale Kundert Kamran Ahmad Karl O. Pinc +Karthikeyan Singaravelan Katarzyna Jachim Katarzyna Król Katerina Koukiou diff --git a/changelog/7527.improvement.rst b/changelog/7527.improvement.rst new file mode 100644 index 00000000000..726acffa9f6 --- /dev/null +++ b/changelog/7527.improvement.rst @@ -0,0 +1 @@ +When a comparison between `namedtuple` instances of the same type fails, pytest now shows the differing field names (possibly nested) instead of their indexes. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 93fa48b8e36..da1ffd15e37 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -110,6 +110,10 @@ def isset(x: Any) -> bool: return isinstance(x, (set, frozenset)) +def isnamedtuple(obj: Any) -> bool: + return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None + + def isdatacls(obj: Any) -> bool: return getattr(obj, "__dataclass_fields__", None) is not None @@ -171,14 +175,20 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: if istext(left) and istext(right): explanation = _diff_text(left, right, verbose) else: - if issequence(left) and issequence(right): + if type(left) == type(right) and ( + isdatacls(left) or isattrs(left) or isnamedtuple(left) + ): + # Note: unlike dataclasses/attrs, namedtuples compare only the + # field values, not the type or field names. But this branch + # intentionally only handles the same-type case, which was often + # used in older code bases before dataclasses/attrs were available. + explanation = _compare_eq_cls(left, right, verbose) + elif issequence(left) and issequence(right): explanation = _compare_eq_sequence(left, right, verbose) elif isset(left) and isset(right): explanation = _compare_eq_set(left, right, verbose) elif isdict(left) and isdict(right): explanation = _compare_eq_dict(left, right, verbose) - elif type(left) == type(right) and (isdatacls(left) or isattrs(left)): - explanation = _compare_eq_cls(left, right, verbose) elif verbose > 0: explanation = _compare_eq_verbose(left, right) if isiterable(left) and isiterable(right): @@ -408,6 +418,10 @@ def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: elif isattrs(left): all_fields = left.__attrs_attrs__ fields_to_check = [field.name for field in all_fields if getattr(field, "eq")] + elif isnamedtuple(left): + fields_to_check = left._fields + else: + assert False indent = " " same = [] diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 02ecaf125e1..289fe5b083f 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1,3 +1,4 @@ +import collections import sys import textwrap from typing import Any @@ -987,6 +988,44 @@ class SimpleDataObjectTwo: assert lines is None +class TestAssert_reprcompare_namedtuple: + def test_namedtuple(self) -> None: + NT = collections.namedtuple("NT", ["a", "b"]) + + left = NT(1, "b") + right = NT(1, "c") + + lines = callequal(left, right) + assert lines == [ + "NT(a=1, b='b') == NT(a=1, b='c')", + "", + "Omitting 1 identical items, use -vv to show", + "Differing attributes:", + "['b']", + "", + "Drill down into differing attribute b:", + " b: 'b' != 'c'", + " - c", + " + b", + "Use -v to get the full diff", + ] + + def test_comparing_two_different_namedtuple(self) -> None: + NT1 = collections.namedtuple("NT1", ["a", "b"]) + NT2 = collections.namedtuple("NT2", ["a", "b"]) + + left = NT1(1, "b") + right = NT2(2, "b") + + lines = callequal(left, right) + # Because the types are different, uses the generic sequence matcher. + assert lines == [ + "NT1(a=1, b='b') == NT2(a=2, b='b')", + "At index 0 diff: 1 != 2", + "Use -v to get the full diff", + ] + + class TestFormatExplanation: def test_special_chars_full(self, pytester: Pytester) -> None: # Issue 453, for the bug this would raise IndexError From 8a38e7a6e8039de93c6f24935effd89f034d9c00 Mon Sep 17 00:00:00 2001 From: Cserna Zsolt Date: Wed, 28 Oct 2020 08:27:43 +0100 Subject: [PATCH 753/823] Fix handling recursive symlinks When pytest was run on a directory containing a recursive symlink it failed with ELOOP as the library was not able to determine the type of the direntry: src/_pytest/main.py:685: in collect if not direntry.is_file(): E OSError: [Errno 40] Too many levels of symbolic links: '/home/florian/proj/pytest/tests/recursive' This is fixed by handling ELOOP and other errors in the visit function in pathlib.py, so the entries whose is_file() call raises an OSError with the pre-defined list of error numbers will be exluded from the result. The _ignore_errors function was copied from Lib/pathlib.py of cpython 3.9. Fixes #7951 --- AUTHORS | 1 + changelog/7951.bugfix.rst | 1 + src/_pytest/pathlib.py | 39 +++++++++++++++++++++++++++++++++++++- testing/test_collection.py | 14 ++++++++++++++ testing/test_pathlib.py | 13 +++++++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 changelog/7951.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 35d220e0044..f8d3d421c9c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -314,3 +314,4 @@ Xuecong Liao Yoav Caspi Zac Hatfield-Dodds Zoltán Máté +Zsolt Cserna diff --git a/changelog/7951.bugfix.rst b/changelog/7951.bugfix.rst new file mode 100644 index 00000000000..56c71db7839 --- /dev/null +++ b/changelog/7951.bugfix.rst @@ -0,0 +1 @@ +Fixed handling of recursive symlinks when collecting tests. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index f0bdb1481bb..a1c36407611 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -9,6 +9,10 @@ import uuid import warnings from enum import Enum +from errno import EBADF +from errno import ELOOP +from errno import ENOENT +from errno import ENOTDIR from functools import partial from os.path import expanduser from os.path import expandvars @@ -37,6 +41,24 @@ _AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath) +# The following function, variables and comments were +# copied from cpython 3.9 Lib/pathlib.py file. + +# EBADF - guard against macOS `stat` throwing EBADF +_IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP) + +_IGNORED_WINERRORS = ( + 21, # ERROR_NOT_READY - drive exists but is not accessible + 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself +) + + +def _ignore_error(exception): + return ( + getattr(exception, "errno", None) in _IGNORED_ERRORS + or getattr(exception, "winerror", None) in _IGNORED_WINERRORS + ) + def get_lock_path(path: _AnyPurePath) -> _AnyPurePath: return path.joinpath(".lock") @@ -555,8 +577,23 @@ def visit( Entries at each directory level are sorted. """ - entries = sorted(os.scandir(path), key=lambda entry: entry.name) + + # Skip entries with symlink loops and other brokenness, so the caller doesn't + # have to deal with it. + entries = [] + for entry in os.scandir(path): + try: + entry.is_file() + except OSError as err: + if _ignore_error(err): + continue + raise + entries.append(entry) + + entries.sort(key=lambda entry: entry.name) + yield from entries + for entry in entries: if entry.is_dir(follow_symlinks=False) and recurse(entry): yield from visit(entry.path, recurse) diff --git a/testing/test_collection.py b/testing/test_collection.py index 841aa358b96..b05048742a1 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1404,3 +1404,17 @@ def a(): return 4 result = testdir.runpytest() # Not INTERNAL_ERROR assert result.ret == ExitCode.INTERRUPTED + + +def test_does_not_crash_on_recursive_symlink(testdir: Testdir) -> None: + """Regression test for an issue around recursive symlinks (#7951).""" + symlink_or_skip("recursive", testdir.tmpdir.join("recursive")) + testdir.makepyfile( + """ + def test_foo(): assert True + """ + ) + result = testdir.runpytest() + + assert result.ret == ExitCode.OK + assert result.parseoutcomes() == {"passed": 1} diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index e37b33847ee..0507e3d6866 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -17,6 +17,8 @@ from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import maybe_delete_a_numbered_dir from _pytest.pathlib import resolve_package_path +from _pytest.pathlib import symlink_or_skip +from _pytest.pathlib import visit class TestFNMatcherPort: @@ -401,3 +403,14 @@ def test_commonpath() -> None: assert commonpath(subpath, path) == path assert commonpath(Path(str(path) + "suffix"), path) == path.parent assert commonpath(path, path.parent.parent) == path.parent.parent + + +def test_visit_ignores_errors(tmpdir) -> None: + symlink_or_skip("recursive", tmpdir.join("recursive")) + tmpdir.join("foo").write_binary(b"") + tmpdir.join("bar").write_binary(b"") + + assert [entry.name for entry in visit(tmpdir, recurse=lambda entry: False)] == [ + "bar", + "foo", + ] From f9d82a34f42d88de46b3151c2a1bb5528bdf80a1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 31 Oct 2020 22:10:51 +0200 Subject: [PATCH 754/823] ci: replace deprecated ::set-env --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5e9367a5d69..ed9547152f6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -165,7 +165,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - name: set PY - run: echo "::set-env name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" + run: echo "name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV - uses: actions/cache@v1 with: path: ~/.cache/pre-commit From 76226182aebcf73445990926e3fcf2a14ee58dcd Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 31 Oct 2020 22:14:16 +0200 Subject: [PATCH 755/823] ci: change cache action to v2 Supposed to be faster. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ed9547152f6..94b02e5dd4f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -166,7 +166,7 @@ jobs: - uses: actions/setup-python@v2 - name: set PY run: echo "name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV - - uses: actions/cache@v1 + - uses: actions/cache@v2 with: path: ~/.cache/pre-commit key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} From 8aa9ea95e1024fd8725d1554f5575eee9333c10c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 31 Oct 2020 22:16:07 +0200 Subject: [PATCH 756/823] ci: test on Python 3.9 final --- .github/workflows/main.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 94b02e5dd4f..79d8ba5d17e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -84,7 +84,7 @@ jobs: os: ubuntu-latest tox_env: "py38-xdist" - name: "ubuntu-py39" - python: "3.9-dev" + python: "3.9" os: ubuntu-latest tox_env: "py39-xdist" - name: "ubuntu-pypy3" @@ -123,12 +123,6 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v2 - if: matrix.python != '3.9-dev' - with: - python-version: ${{ matrix.python }} - - name: Set up Python ${{ matrix.python }} (deadsnakes) - uses: deadsnakes/action@v2.0.0 - if: matrix.python == '3.9-dev' with: python-version: ${{ matrix.python }} - name: Install dependencies From 489f6f4499f76f85c9b4dc8e7bb53d6c90ec0397 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 1 Nov 2020 15:09:32 +0200 Subject: [PATCH 757/823] unittest: fix quadratic behavior in collection of unittests using setUpClass/setup_method This is similar to 50114d4731876dae; I missed that unittest does the same thing. --- bench/unit_test.py | 13 +++++++++++++ src/_pytest/unittest.py | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 bench/unit_test.py diff --git a/bench/unit_test.py b/bench/unit_test.py new file mode 100644 index 00000000000..ad52069dbfd --- /dev/null +++ b/bench/unit_test.py @@ -0,0 +1,13 @@ +from unittest import TestCase # noqa: F401 + +for i in range(15000): + exec( + f""" +class Test{i}(TestCase): + @classmethod + def setUpClass(cls): pass + def test_1(self): pass + def test_2(self): pass + def test_3(self): pass +""" + ) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 6dc404a3949..21db0ec23f9 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -119,7 +119,12 @@ def _make_xunit_fixture( if setup is None and teardown is None: return None - @pytest.fixture(scope=scope, autouse=True) + @pytest.fixture( + scope=scope, + autouse=True, + # Use a unique name to speed up lookup. + name=f"unittest_{setup_name}_fixture_{obj.__qualname__}", + ) def fixture(self, request: FixtureRequest) -> Generator[None, None, None]: if _is_skipped(self): reason = self.__unittest_skip_why__ From b815f430e5d7e244575eedc448d793455d4f9e81 Mon Sep 17 00:00:00 2001 From: duthades Date: Wed, 4 Nov 2020 21:55:07 +0530 Subject: [PATCH 758/823] #7942 test_session.py migrate from testdir to pytester - Add name to AUTHORS --- AUTHORS | 1 + testing/test_session.py | 160 ++++++++++++++++++++-------------------- 2 files changed, 83 insertions(+), 78 deletions(-) diff --git a/AUTHORS b/AUTHORS index 0ece0523518..9657d0866ac 100644 --- a/AUTHORS +++ b/AUTHORS @@ -261,6 +261,7 @@ Ryan Wooden Samuel Dion-Girardeau Samuel Searles-Bryant Samuele Pedroni +Sanket Duthade Sankt Petersbug Segev Finer Serhii Mozghovyi diff --git a/testing/test_session.py b/testing/test_session.py index 446d764c395..5389e5b2b19 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -1,10 +1,12 @@ import pytest from _pytest.config import ExitCode +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Pytester class SessionTests: - def test_basic_testitem_events(self, testdir): - tfile = testdir.makepyfile( + def test_basic_testitem_events(self, pytester: Pytester) -> None: + tfile = pytester.makepyfile( """ def test_one(): pass @@ -17,7 +19,7 @@ def test_two(self, someargs): pass """ ) - reprec = testdir.inline_run(tfile) + reprec = pytester.inline_run(tfile) passed, skipped, failed = reprec.listoutcomes() assert len(skipped) == 0 assert len(passed) == 1 @@ -35,8 +37,8 @@ def end(x): # assert len(colreports) == 4 # assert colreports[1].report.failed - def test_nested_import_error(self, testdir): - tfile = testdir.makepyfile( + def test_nested_import_error(self, pytester: Pytester) -> None: + tfile = pytester.makepyfile( """ import import_fails def test_this(): @@ -47,14 +49,14 @@ def test_this(): a = 1 """, ) - reprec = testdir.inline_run(tfile) + reprec = pytester.inline_run(tfile) values = reprec.getfailedcollections() assert len(values) == 1 out = str(values[0].longrepr) assert out.find("does_not_work") != -1 - def test_raises_output(self, testdir): - reprec = testdir.inline_runsource( + def test_raises_output(self, pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ import pytest def test_raises_doesnt(): @@ -63,18 +65,18 @@ def test_raises_doesnt(): ) passed, skipped, failed = reprec.listoutcomes() assert len(failed) == 1 - out = failed[0].longrepr.reprcrash.message + out = failed[0].longrepr.reprcrash.message # type: ignore[union-attr] assert "DID NOT RAISE" in out - def test_syntax_error_module(self, testdir): - reprec = testdir.inline_runsource("this is really not python") + def test_syntax_error_module(self, pytester: Pytester) -> None: + reprec = pytester.inline_runsource("this is really not python") values = reprec.getfailedcollections() assert len(values) == 1 out = str(values[0].longrepr) assert out.find("not python") != -1 - def test_exit_first_problem(self, testdir): - reprec = testdir.inline_runsource( + def test_exit_first_problem(self, pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ def test_one(): assert 0 def test_two(): assert 0 @@ -85,8 +87,8 @@ def test_two(): assert 0 assert failed == 1 assert passed == skipped == 0 - def test_maxfail(self, testdir): - reprec = testdir.inline_runsource( + def test_maxfail(self, pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ def test_one(): assert 0 def test_two(): assert 0 @@ -98,8 +100,8 @@ def test_three(): assert 0 assert failed == 2 assert passed == skipped == 0 - def test_broken_repr(self, testdir): - p = testdir.makepyfile( + def test_broken_repr(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest @@ -124,14 +126,14 @@ def test_implicit_bad_repr1(self): """ ) - reprec = testdir.inline_run(p) + reprec = pytester.inline_run(p) passed, skipped, failed = reprec.listoutcomes() assert (len(passed), len(skipped), len(failed)) == (1, 0, 1) - out = failed[0].longrepr.reprcrash.message + out = failed[0].longrepr.reprcrash.message # type: ignore[union-attr] assert out.find("<[reprexc() raised in repr()] BrokenRepr1") != -1 - def test_broken_repr_with_showlocals_verbose(self, testdir): - p = testdir.makepyfile( + def test_broken_repr_with_showlocals_verbose(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ class ObjWithErrorInRepr: def __repr__(self): @@ -142,10 +144,10 @@ def test_repr_error(): assert x == "value" """ ) - reprec = testdir.inline_run("--showlocals", "-vv", p) + reprec = pytester.inline_run("--showlocals", "-vv", p) passed, skipped, failed = reprec.listoutcomes() assert (len(passed), len(skipped), len(failed)) == (0, 0, 1) - entries = failed[0].longrepr.reprtraceback.reprentries + entries = failed[0].longrepr.reprtraceback.reprentries # type: ignore[union-attr] assert len(entries) == 1 repr_locals = entries[0].reprlocals assert repr_locals.lines @@ -154,8 +156,8 @@ def test_repr_error(): "x = <[NotImplementedError() raised in repr()] ObjWithErrorInRepr" ) - def test_skip_file_by_conftest(self, testdir): - testdir.makepyfile( + def test_skip_file_by_conftest(self, pytester: Pytester) -> None: + pytester.makepyfile( conftest=""" import pytest def pytest_collect_file(): @@ -166,7 +168,7 @@ def test_one(): pass """, ) try: - reprec = testdir.inline_run(testdir.tmpdir) + reprec = pytester.inline_run(pytester.path) except pytest.skip.Exception: # pragma: no cover pytest.fail("wrong skipped caught") reports = reprec.getreports("pytest_collectreport") @@ -175,8 +177,8 @@ def test_one(): pass class TestNewSession(SessionTests): - def test_order_of_execution(self, testdir): - reprec = testdir.inline_runsource( + def test_order_of_execution(self, pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """ values = [] def test_1(): @@ -201,8 +203,8 @@ def test_4(self): assert failed == skipped == 0 assert passed == 7 - def test_collect_only_with_various_situations(self, testdir): - p = testdir.makepyfile( + def test_collect_only_with_various_situations(self, pytester: Pytester) -> None: + p = pytester.makepyfile( test_one=""" def test_one(): raise ValueError() @@ -217,7 +219,7 @@ class TestY(TestX): test_three="xxxdsadsadsadsa", __init__="", ) - reprec = testdir.inline_run("--collect-only", p.dirpath()) + reprec = pytester.inline_run("--collect-only", p.parent) itemstarted = reprec.getcalls("pytest_itemcollected") assert len(itemstarted) == 3 @@ -229,66 +231,66 @@ class TestY(TestX): colfail = [x for x in finished if x.failed] assert len(colfail) == 1 - def test_minus_x_import_error(self, testdir): - testdir.makepyfile(__init__="") - testdir.makepyfile(test_one="xxxx", test_two="yyyy") - reprec = testdir.inline_run("-x", testdir.tmpdir) + def test_minus_x_import_error(self, pytester: Pytester) -> None: + pytester.makepyfile(__init__="") + pytester.makepyfile(test_one="xxxx", test_two="yyyy") + reprec = pytester.inline_run("-x", pytester.path) finished = reprec.getreports("pytest_collectreport") colfail = [x for x in finished if x.failed] assert len(colfail) == 1 - def test_minus_x_overridden_by_maxfail(self, testdir): - testdir.makepyfile(__init__="") - testdir.makepyfile(test_one="xxxx", test_two="yyyy", test_third="zzz") - reprec = testdir.inline_run("-x", "--maxfail=2", testdir.tmpdir) + def test_minus_x_overridden_by_maxfail(self, pytester: Pytester) -> None: + pytester.makepyfile(__init__="") + pytester.makepyfile(test_one="xxxx", test_two="yyyy", test_third="zzz") + reprec = pytester.inline_run("-x", "--maxfail=2", pytester.path) finished = reprec.getreports("pytest_collectreport") colfail = [x for x in finished if x.failed] assert len(colfail) == 2 -def test_plugin_specify(testdir): +def test_plugin_specify(pytester: Pytester) -> None: with pytest.raises(ImportError): - testdir.parseconfig("-p", "nqweotexistent") + pytester.parseconfig("-p", "nqweotexistent") # pytest.raises(ImportError, # "config.do_configure(config)" # ) -def test_plugin_already_exists(testdir): - config = testdir.parseconfig("-p", "terminal") +def test_plugin_already_exists(pytester: Pytester) -> None: + config = pytester.parseconfig("-p", "terminal") assert config.option.plugins == ["terminal"] config._do_configure() config._ensure_unconfigure() -def test_exclude(testdir): - hellodir = testdir.mkdir("hello") - hellodir.join("test_hello.py").write("x y syntaxerror") - hello2dir = testdir.mkdir("hello2") - hello2dir.join("test_hello2.py").write("x y syntaxerror") - testdir.makepyfile(test_ok="def test_pass(): pass") - result = testdir.runpytest("--ignore=hello", "--ignore=hello2") +def test_exclude(pytester: Pytester) -> None: + hellodir = pytester.mkdir("hello") + hellodir.joinpath("test_hello.py").write_text("x y syntaxerror") + hello2dir = pytester.mkdir("hello2") + hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror") + pytester.makepyfile(test_ok="def test_pass(): pass") + result = pytester.runpytest("--ignore=hello", "--ignore=hello2") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) -def test_exclude_glob(testdir): - hellodir = testdir.mkdir("hello") - hellodir.join("test_hello.py").write("x y syntaxerror") - hello2dir = testdir.mkdir("hello2") - hello2dir.join("test_hello2.py").write("x y syntaxerror") - hello3dir = testdir.mkdir("hallo3") - hello3dir.join("test_hello3.py").write("x y syntaxerror") - subdir = testdir.mkdir("sub") - subdir.join("test_hello4.py").write("x y syntaxerror") - testdir.makepyfile(test_ok="def test_pass(): pass") - result = testdir.runpytest("--ignore-glob=*h[ea]llo*") +def test_exclude_glob(pytester: Pytester) -> None: + hellodir = pytester.mkdir("hello") + hellodir.joinpath("test_hello.py").write_text("x y syntaxerror") + hello2dir = pytester.mkdir("hello2") + hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror") + hello3dir = pytester.mkdir("hallo3") + hello3dir.joinpath("test_hello3.py").write_text("x y syntaxerror") + subdir = pytester.mkdir("sub") + subdir.joinpath("test_hello4.py").write_text("x y syntaxerror") + pytester.makepyfile(test_ok="def test_pass(): pass") + result = pytester.runpytest("--ignore-glob=*h[ea]llo*") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) -def test_deselect(testdir): - testdir.makepyfile( +def test_deselect(pytester: Pytester) -> None: + pytester.makepyfile( test_a=""" import pytest @@ -303,7 +305,7 @@ def test_c1(self): pass def test_c2(self): pass """ ) - result = testdir.runpytest( + result = pytester.runpytest( "-v", "--deselect=test_a.py::test_a2[1]", "--deselect=test_a.py::test_a2[2]", @@ -315,8 +317,8 @@ def test_c2(self): pass assert not line.startswith(("test_a.py::test_a2[1]", "test_a.py::test_a2[2]")) -def test_sessionfinish_with_start(testdir): - testdir.makeconftest( +def test_sessionfinish_with_start(pytester: Pytester) -> None: + pytester.makeconftest( """ import os values = [] @@ -329,18 +331,20 @@ def pytest_sessionfinish(): """ ) - res = testdir.runpytest("--collect-only") + res = pytester.runpytest("--collect-only") assert res.ret == ExitCode.NO_TESTS_COLLECTED @pytest.mark.parametrize("path", ["root", "{relative}/root", "{environment}/root"]) -def test_rootdir_option_arg(testdir, monkeypatch, path): - monkeypatch.setenv("PY_ROOTDIR_PATH", str(testdir.tmpdir)) - path = path.format(relative=str(testdir.tmpdir), environment="$PY_ROOTDIR_PATH") - - rootdir = testdir.mkdir("root") - rootdir.mkdir("tests") - testdir.makepyfile( +def test_rootdir_option_arg( + pytester: Pytester, monkeypatch: MonkeyPatch, path: str +) -> None: + monkeypatch.setenv("PY_ROOTDIR_PATH", str(pytester.path)) + path = path.format(relative=str(pytester.path), environment="$PY_ROOTDIR_PATH") + + rootdir = pytester.path / "root" / "tests" + rootdir.mkdir(parents=True) + pytester.makepyfile( """ import os def test_one(): @@ -348,18 +352,18 @@ def test_one(): """ ) - result = testdir.runpytest(f"--rootdir={path}") + result = pytester.runpytest(f"--rootdir={path}") result.stdout.fnmatch_lines( [ - f"*rootdir: {testdir.tmpdir}/root", + f"*rootdir: {pytester.path}/root", "root/test_rootdir_option_arg.py *", "*1 passed*", ] ) -def test_rootdir_wrong_option_arg(testdir): - result = testdir.runpytest("--rootdir=wrong_dir") +def test_rootdir_wrong_option_arg(pytester: Pytester) -> None: + result = pytester.runpytest("--rootdir=wrong_dir") result.stderr.fnmatch_lines( ["*Directory *wrong_dir* not found. Check your '--rootdir' option.*"] ) From 070f8e0f9d9ffaea6d36a1d91e7f6639b6672cd1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 5 Nov 2020 16:08:54 +0200 Subject: [PATCH 759/823] testing: silence deprecation warning from older pyparsing releases This causes some tests to fail when using these older versions. --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index dce93a6065d..2b25431111e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,8 @@ xfail_strict = true filterwarnings = [ "error", "default:Using or importing the ABCs:DeprecationWarning:unittest2.*", + # produced by older pyparsing<=2.2.0. + "default:Using or importing the ABCs:DeprecationWarning:pyparsing.*", "default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*", "ignore:Module already imported so cannot be rewritten:pytest.PytestWarning", # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8)." From 30287b49cd02b6a14ade1bbe9adf9711c3d1259e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 6 Nov 2020 05:48:20 -0300 Subject: [PATCH 760/823] Deprecate --strict (#7985) Fix #7530 --- changelog/7530.deprecation.rst | 4 ++++ doc/en/deprecations.rst | 13 +++++++++++++ src/_pytest/config/__init__.py | 5 +++++ src/_pytest/deprecated.py | 4 ++++ src/_pytest/main.py | 4 +++- src/_pytest/mark/structures.py | 2 +- testing/deprecated_test.py | 20 ++++++++++++++++++++ 7 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 changelog/7530.deprecation.rst diff --git a/changelog/7530.deprecation.rst b/changelog/7530.deprecation.rst new file mode 100644 index 00000000000..36a763e51f1 --- /dev/null +++ b/changelog/7530.deprecation.rst @@ -0,0 +1,4 @@ +The ``--strict`` command-line option has been deprecated, use ``--strict-markers`` instead. + +We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing flag for all strictness +related options (``--strict-markers`` and ``--strict-config`` at the moment, more might be introduced in the future). diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 14d1eeb98af..d588b1bea8a 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -18,6 +18,19 @@ Deprecated Features Below is a complete list of all pytest features which are considered deprecated. Using those features will issue :class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. +The ``--strict`` command-line option +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.2 + +The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which +better conveys what the option does. + +We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing +flag for all strictness related options (``--strict-markers`` and ``--strict-config`` +at the moment, more might be introduced in the future). + + The ``pytest_warning_captured`` hook ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 7e486e99e50..6c1d9c69a50 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1177,6 +1177,11 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: self._validate_plugins() self._warn_about_skipped_plugins() + if self.known_args_namespace.strict: + self.issue_config_time_warning( + _pytest.deprecated.STRICT_OPTION, stacklevel=2 + ) + if self.known_args_namespace.confcutdir is None and self.inipath is not None: confcutdir = str(self.inipath.parent) self.known_args_namespace.confcutdir = confcutdir diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index fd00fe2d6d5..a9a162f41fd 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -51,3 +51,7 @@ "The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; " "use self.session.gethookproxy() and self.session.isinitpath() instead. " ) + +STRICT_OPTION = PytestDeprecationWarning( + "The --strict option is deprecated, use --strict-markers instead." +) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d8a208a1a2e..04b51ac00fb 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -101,10 +101,12 @@ def pytest_addoption(parser: Parser) -> None: ) group._addoption( "--strict-markers", - "--strict", action="store_true", help="markers not registered in the `markers` section of the configuration file raise errors.", ) + group._addoption( + "--strict", action="store_true", help="(deprecated) alias to --strict-markers.", + ) group._addoption( "-c", metavar="file", diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 6cbdf8b3066..6c126cf4a29 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -496,7 +496,7 @@ def __getattr__(self, name: str) -> MarkDecorator: # If the name is not in the set of known marks after updating, # then it really is time to issue a warning or an error. if name not in self._markers: - if self._config.option.strict_markers: + if self._config.option.strict_markers or self._config.option.strict: fail( f"{name!r} not found in `markers` configuration option", pytrace=False, diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 5fe9ad7305f..e0bc1db70a0 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -4,6 +4,7 @@ import pytest from _pytest import deprecated +from _pytest.pytester import Pytester from _pytest.pytester import Testdir @@ -95,3 +96,22 @@ def test_foo(): pass session.gethookproxy(testdir.tmpdir) session.isinitpath(testdir.tmpdir) assert len(rec) == 0 + + +def test_strict_option_is_deprecated(pytester: Pytester) -> None: + """--strict is a deprecated alias to --strict-markers (#7530).""" + pytester.makepyfile( + """ + import pytest + + @pytest.mark.unknown + def test_foo(): pass + """ + ) + result = pytester.runpytest("--strict") + result.stdout.fnmatch_lines( + [ + "'unknown' not found in `markers` configuration option", + "*PytestDeprecationWarning: The --strict option is deprecated, use --strict-markers instead.", + ] + ) From a73fb6e006cc2ad1c47dc0653688de62c27b57d9 Mon Sep 17 00:00:00 2001 From: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Date: Fri, 6 Nov 2020 14:29:12 +0000 Subject: [PATCH 761/823] Add pythonenv* to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index faea9eac03f..be7ac82dd8c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ issue/ env/ .env/ .venv/ +pythonenv* 3rdparty/ .tox .cache From 6a5037a25b98106c1e8e325ae939ffb67269c58b Mon Sep 17 00:00:00 2001 From: Garvit Shubham <70941313+itsmegarvi@users.noreply.github.com> Date: Sat, 7 Nov 2020 17:59:45 +0530 Subject: [PATCH 762/823] #7942 test_setupplan.py migrate from testdir to Pytester (#8004) Co-authored-by: Bruno Oliveira --- AUTHORS | 1 + testing/test_setupplan.py | 29 +++++++++++++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/AUTHORS b/AUTHORS index 9657d0866ac..8febe36ef0a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -109,6 +109,7 @@ Florian Bruhin Florian Dahlitz Floris Bruynooghe Gabriel Reis +Garvit Shubham Gene Wood George Kussumoto Georgy Dyuldin diff --git a/testing/test_setupplan.py b/testing/test_setupplan.py index 929e883cce2..d51a1873959 100644 --- a/testing/test_setupplan.py +++ b/testing/test_setupplan.py @@ -1,6 +1,11 @@ -def test_show_fixtures_and_test(testdir, dummy_yaml_custom_test): +from _pytest.pytester import Pytester + + +def test_show_fixtures_and_test( + pytester: Pytester, dummy_yaml_custom_test: None +) -> None: """Verify that fixtures are not executed.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.fixture @@ -11,7 +16,7 @@ def test_arg(arg): """ ) - result = testdir.runpytest("--setup-plan") + result = pytester.runpytest("--setup-plan") assert result.ret == 0 result.stdout.fnmatch_lines( @@ -19,7 +24,9 @@ def test_arg(arg): ) -def test_show_multi_test_fixture_setup_and_teardown_correctly_simple(testdir): +def test_show_multi_test_fixture_setup_and_teardown_correctly_simple( + pytester: Pytester, +) -> None: """Verify that when a fixture lives for longer than a single test, --setup-plan correctly displays the SETUP/TEARDOWN indicators the right number of times. @@ -31,7 +38,7 @@ def test_show_multi_test_fixture_setup_and_teardown_correctly_simple(testdir): correct fixture lifetimes. It was purely a display bug for --setup-plan, and did not affect the related --setup-show or --setup-only.) """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.fixture(scope = 'class') @@ -45,7 +52,7 @@ def test_two(self, fix): """ ) - result = testdir.runpytest("--setup-plan") + result = pytester.runpytest("--setup-plan") assert result.ret == 0 setup_fragment = "SETUP C fix" @@ -66,9 +73,11 @@ def test_two(self, fix): assert teardown_count == 1 -def test_show_multi_test_fixture_setup_and_teardown_same_as_setup_show(testdir): +def test_show_multi_test_fixture_setup_and_teardown_same_as_setup_show( + pytester: Pytester, +) -> None: """Verify that SETUP/TEARDOWN messages match what comes out of --setup-show.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.fixture(scope = 'session') @@ -93,8 +102,8 @@ def test_two(self, sess, mod, cls, func): """ ) - plan_result = testdir.runpytest("--setup-plan") - show_result = testdir.runpytest("--setup-show") + plan_result = pytester.runpytest("--setup-plan") + show_result = pytester.runpytest("--setup-show") # the number and text of these lines should be identical plan_lines = [ From 3bcd316f076b185bcc89c41a41345861f752aff7 Mon Sep 17 00:00:00 2001 From: Sanket Duthade Date: Sat, 7 Nov 2020 20:26:00 +0530 Subject: [PATCH 763/823] test_collection.py migrate from testdir to Pytester (#8003) --- src/_pytest/pytester.py | 7 +- testing/test_collection.py | 716 +++++++++++++++++++------------------ 2 files changed, 376 insertions(+), 347 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 935be84c122..43ccc97c693 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -50,7 +50,6 @@ from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.pathlib import make_numbered_dir -from _pytest.python import Module from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.tmpdir import TempPathFactory @@ -652,7 +651,7 @@ def __init__( ) -> None: self._request = request self._mod_collections: WeakKeyDictionary[ - Module, List[Union[Item, Collector]] + Collector, List[Union[Item, Collector]] ] = (WeakKeyDictionary()) if request.function: name: str = request.function.__name__ @@ -1244,7 +1243,7 @@ def getmodulecol( return self.getnode(config, path) def collect_by_name( - self, modcol: Module, name: str + self, modcol: Collector, name: str ) -> Optional[Union[Item, Collector]]: """Return the collection node for name from the module collection. @@ -1639,7 +1638,7 @@ def getmodulecol(self, source, configargs=(), withinit=False): ) def collect_by_name( - self, modcol: Module, name: str + self, modcol: Collector, name: str ) -> Optional[Union[Item, Collector]]: """See :meth:`Pytester.collect_by_name`.""" return self._pytester.collect_by_name(modcol, name) diff --git a/testing/test_collection.py b/testing/test_collection.py index 2fa74605fbb..1138c2bd6f5 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,42 +1,56 @@ import os import pprint +import shutil import sys import textwrap from pathlib import Path +from typing import List import pytest from _pytest.config import ExitCode +from _pytest.fixtures import FixtureRequest from _pytest.main import _in_venv from _pytest.main import Session +from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Item from _pytest.pathlib import symlink_or_skip +from _pytest.pytester import HookRecorder from _pytest.pytester import Pytester from _pytest.pytester import Testdir +def ensure_file(file_path: Path) -> Path: + """Ensure that file exists""" + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.touch(exist_ok=True) + return file_path + + class TestCollector: - def test_collect_versus_item(self): - from pytest import Collector, Item + def test_collect_versus_item(self) -> None: + from pytest import Collector + from pytest import Item assert not issubclass(Collector, Item) assert not issubclass(Item, Collector) - def test_check_equality(self, testdir: Testdir) -> None: - modcol = testdir.getmodulecol( + def test_check_equality(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol( """ def test_pass(): pass def test_fail(): assert 0 """ ) - fn1 = testdir.collect_by_name(modcol, "test_pass") + fn1 = pytester.collect_by_name(modcol, "test_pass") assert isinstance(fn1, pytest.Function) - fn2 = testdir.collect_by_name(modcol, "test_pass") + fn2 = pytester.collect_by_name(modcol, "test_pass") assert isinstance(fn2, pytest.Function) assert fn1 == fn2 assert fn1 != modcol assert hash(fn1) == hash(fn2) - fn3 = testdir.collect_by_name(modcol, "test_fail") + fn3 = pytester.collect_by_name(modcol, "test_fail") assert isinstance(fn3, pytest.Function) assert not (fn1 == fn3) assert fn1 != fn3 @@ -49,31 +63,35 @@ def test_fail(): assert 0 assert [1, 2, 3] != fn # type: ignore[comparison-overlap] assert modcol != fn - assert testdir.collect_by_name(modcol, "doesnotexist") is None + assert pytester.collect_by_name(modcol, "doesnotexist") is None - def test_getparent(self, testdir): - modcol = testdir.getmodulecol( + def test_getparent(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol( """ class TestClass: def test_foo(self): pass """ ) - cls = testdir.collect_by_name(modcol, "TestClass") - fn = testdir.collect_by_name(testdir.collect_by_name(cls, "()"), "test_foo") + cls = pytester.collect_by_name(modcol, "TestClass") + assert isinstance(cls, pytest.Class) + instance = pytester.collect_by_name(cls, "()") + assert isinstance(instance, pytest.Instance) + fn = pytester.collect_by_name(instance, "test_foo") + assert isinstance(fn, pytest.Function) - parent = fn.getparent(pytest.Module) - assert parent is modcol + module_parent = fn.getparent(pytest.Module) + assert module_parent is modcol - parent = fn.getparent(pytest.Function) - assert parent is fn + function_parent = fn.getparent(pytest.Function) + assert function_parent is fn - parent = fn.getparent(pytest.Class) - assert parent is cls + class_parent = fn.getparent(pytest.Class) + assert class_parent is cls - def test_getcustomfile_roundtrip(self, testdir): - hello = testdir.makefile(".xxx", hello="world") - testdir.makepyfile( + def test_getcustomfile_roundtrip(self, pytester: Pytester) -> None: + hello = pytester.makefile(".xxx", hello="world") + pytester.makepyfile( conftest=""" import pytest class CustomFile(pytest.File): @@ -83,16 +101,16 @@ def pytest_collect_file(path, parent): return CustomFile.from_parent(fspath=path, parent=parent) """ ) - node = testdir.getpathnode(hello) + node = pytester.getpathnode(hello) assert isinstance(node, pytest.File) assert node.name == "hello.xxx" nodes = node.session.perform_collect([node.nodeid], genitems=False) assert len(nodes) == 1 assert isinstance(nodes[0], pytest.File) - def test_can_skip_class_with_test_attr(self, testdir): + def test_can_skip_class_with_test_attr(self, pytester: Pytester) -> None: """Assure test class is skipped when using `__test__=False` (See #2007).""" - testdir.makepyfile( + pytester.makepyfile( """ class TestFoo(object): __test__ = False @@ -102,25 +120,25 @@ def test_foo(): assert True """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["collected 0 items", "*no tests ran in*"]) class TestCollectFS: - def test_ignored_certain_directories(self, testdir): - tmpdir = testdir.tmpdir - tmpdir.ensure("build", "test_notfound.py") - tmpdir.ensure("dist", "test_notfound.py") - tmpdir.ensure("_darcs", "test_notfound.py") - tmpdir.ensure("CVS", "test_notfound.py") - tmpdir.ensure("{arch}", "test_notfound.py") - tmpdir.ensure(".whatever", "test_notfound.py") - tmpdir.ensure(".bzr", "test_notfound.py") - tmpdir.ensure("normal", "test_found.py") + def test_ignored_certain_directories(self, pytester: Pytester) -> None: + tmpdir = pytester.path + ensure_file(tmpdir / "build" / "test_notfound.py") + ensure_file(tmpdir / "dist" / "test_notfound.py") + ensure_file(tmpdir / "_darcs" / "test_notfound.py") + ensure_file(tmpdir / "CVS" / "test_notfound.py") + ensure_file(tmpdir / "{arch}" / "test_notfound.py") + ensure_file(tmpdir / ".whatever" / "test_notfound.py") + ensure_file(tmpdir / ".bzr" / "test_notfound.py") + ensure_file(tmpdir / "normal" / "test_found.py") for x in Path(str(tmpdir)).rglob("test_*.py"): x.write_text("def test_hello(): pass", "utf-8") - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") s = result.stdout.str() assert "test_notfound" not in s assert "test_found" in s @@ -136,20 +154,20 @@ def test_ignored_certain_directories(self, testdir): "Activate.ps1", ), ) - def test_ignored_virtualenvs(self, testdir, fname): + def test_ignored_virtualenvs(self, pytester: Pytester, fname: str) -> None: bindir = "Scripts" if sys.platform.startswith("win") else "bin" - testdir.tmpdir.ensure("virtual", bindir, fname) - testfile = testdir.tmpdir.ensure("virtual", "test_invenv.py") - testfile.write("def test_hello(): pass") + ensure_file(pytester.path / "virtual" / bindir / fname) + testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py") + testfile.write_text("def test_hello(): pass") # by default, ignore tests inside a virtualenv - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.no_fnmatch_line("*test_invenv*") # allow test collection if user insists - result = testdir.runpytest("--collect-in-virtualenv") + result = pytester.runpytest("--collect-in-virtualenv") assert "test_invenv" in result.stdout.str() # allow test collection if user directly passes in the directory - result = testdir.runpytest("virtual") + result = pytester.runpytest("virtual") assert "test_invenv" in result.stdout.str() @pytest.mark.parametrize( @@ -163,16 +181,18 @@ def test_ignored_virtualenvs(self, testdir, fname): "Activate.ps1", ), ) - def test_ignored_virtualenvs_norecursedirs_precedence(self, testdir, fname): + def test_ignored_virtualenvs_norecursedirs_precedence( + self, pytester: Pytester, fname: str + ) -> None: bindir = "Scripts" if sys.platform.startswith("win") else "bin" # norecursedirs takes priority - testdir.tmpdir.ensure(".virtual", bindir, fname) - testfile = testdir.tmpdir.ensure(".virtual", "test_invenv.py") - testfile.write("def test_hello(): pass") - result = testdir.runpytest("--collect-in-virtualenv") + ensure_file(pytester.path / ".virtual" / bindir / fname) + testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py") + testfile.write_text("def test_hello(): pass") + result = pytester.runpytest("--collect-in-virtualenv") result.stdout.no_fnmatch_line("*test_invenv*") # ...unless the virtualenv is explicitly given on the CLI - result = testdir.runpytest("--collect-in-virtualenv", ".virtual") + result = pytester.runpytest("--collect-in-virtualenv", ".virtual") assert "test_invenv" in result.stdout.str() @pytest.mark.parametrize( @@ -186,7 +206,7 @@ def test_ignored_virtualenvs_norecursedirs_precedence(self, testdir, fname): "Activate.ps1", ), ) - def test__in_venv(self, testdir, fname): + def test__in_venv(self, testdir: Testdir, fname: str) -> None: """Directly test the virtual env detection function""" bindir = "Scripts" if sys.platform.startswith("win") else "bin" # no bin/activate, not a virtualenv @@ -196,55 +216,55 @@ def test__in_venv(self, testdir, fname): base_path.ensure(bindir, fname) assert _in_venv(base_path) is True - def test_custom_norecursedirs(self, testdir): - testdir.makeini( + def test_custom_norecursedirs(self, pytester: Pytester) -> None: + pytester.makeini( """ [pytest] norecursedirs = mydir xyz* """ ) - tmpdir = testdir.tmpdir - tmpdir.ensure("mydir", "test_hello.py").write("def test_1(): pass") - tmpdir.ensure("xyz123", "test_2.py").write("def test_2(): 0/0") - tmpdir.ensure("xy", "test_ok.py").write("def test_3(): pass") - rec = testdir.inline_run() + tmpdir = pytester.path + ensure_file(tmpdir / "mydir" / "test_hello.py").write_text("def test_1(): pass") + ensure_file(tmpdir / "xyz123" / "test_2.py").write_text("def test_2(): 0/0") + ensure_file(tmpdir / "xy" / "test_ok.py").write_text("def test_3(): pass") + rec = pytester.inline_run() rec.assertoutcome(passed=1) - rec = testdir.inline_run("xyz123/test_2.py") + rec = pytester.inline_run("xyz123/test_2.py") rec.assertoutcome(failed=1) - def test_testpaths_ini(self, testdir, monkeypatch): - testdir.makeini( + def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None: + pytester.makeini( """ [pytest] testpaths = gui uts """ ) - tmpdir = testdir.tmpdir - tmpdir.ensure("env", "test_1.py").write("def test_env(): pass") - tmpdir.ensure("gui", "test_2.py").write("def test_gui(): pass") - tmpdir.ensure("uts", "test_3.py").write("def test_uts(): pass") + tmpdir = pytester.path + ensure_file(tmpdir / "env" / "test_1.py").write_text("def test_env(): pass") + ensure_file(tmpdir / "gui" / "test_2.py").write_text("def test_gui(): pass") + ensure_file(tmpdir / "uts" / "test_3.py").write_text("def test_uts(): pass") # executing from rootdir only tests from `testpaths` directories # are collected - items, reprec = testdir.inline_genitems("-v") + items, reprec = pytester.inline_genitems("-v") assert [x.name for x in items] == ["test_gui", "test_uts"] # check that explicitly passing directories in the command-line # collects the tests for dirname in ("env", "gui", "uts"): - items, reprec = testdir.inline_genitems(tmpdir.join(dirname)) + items, reprec = pytester.inline_genitems(tmpdir.joinpath(dirname)) assert [x.name for x in items] == ["test_%s" % dirname] # changing cwd to each subdirectory and running pytest without # arguments collects the tests in that directory normally for dirname in ("env", "gui", "uts"): - monkeypatch.chdir(testdir.tmpdir.join(dirname)) - items, reprec = testdir.inline_genitems() + monkeypatch.chdir(pytester.path.joinpath(dirname)) + items, reprec = pytester.inline_genitems() assert [x.name for x in items] == ["test_%s" % dirname] class TestCollectPluginHookRelay: - def test_pytest_collect_file(self, testdir): + def test_pytest_collect_file(self, testdir: Testdir) -> None: wascalled = [] class Plugin: @@ -254,19 +274,19 @@ def pytest_collect_file(self, path): wascalled.append(path) testdir.makefile(".abc", "xyz") - pytest.main([testdir.tmpdir], plugins=[Plugin()]) + pytest.main(testdir.tmpdir, plugins=[Plugin()]) assert len(wascalled) == 1 assert wascalled[0].ext == ".abc" class TestPrunetraceback: - def test_custom_repr_failure(self, testdir): - p = testdir.makepyfile( + def test_custom_repr_failure(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import not_exists """ ) - testdir.makeconftest( + pytester.makeconftest( """ import pytest def pytest_collect_file(path, parent): @@ -283,17 +303,17 @@ def repr_failure(self, excinfo): """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*ERROR collecting*", "*hello world*"]) @pytest.mark.xfail(reason="other mechanism for adding to reporting needed") - def test_collect_report_postprocessing(self, testdir): - p = testdir.makepyfile( + def test_collect_report_postprocessing(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import not_exists """ ) - testdir.makeconftest( + pytester.makeconftest( """ import pytest @pytest.hookimpl(hookwrapper=True) @@ -304,45 +324,45 @@ def pytest_make_collect_report(): outcome.force_result(rep) """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*ERROR collecting*", "*header1*"]) class TestCustomConftests: - def test_ignore_collect_path(self, testdir): - testdir.makeconftest( + def test_ignore_collect_path(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_ignore_collect(path, config): return path.basename.startswith("x") or \ path.basename == "test_one.py" """ ) - sub = testdir.mkdir("xy123") - sub.ensure("test_hello.py").write("syntax error") - sub.join("conftest.py").write("syntax error") - testdir.makepyfile("def test_hello(): pass") - testdir.makepyfile(test_one="syntax error") - result = testdir.runpytest("--fulltrace") + sub = pytester.mkdir("xy123") + ensure_file(sub / "test_hello.py").write_text("syntax error") + sub.joinpath("conftest.py").write_text("syntax error") + pytester.makepyfile("def test_hello(): pass") + pytester.makepyfile(test_one="syntax error") + result = pytester.runpytest("--fulltrace") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) - def test_ignore_collect_not_called_on_argument(self, testdir): - testdir.makeconftest( + def test_ignore_collect_not_called_on_argument(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_ignore_collect(path, config): return True """ ) - p = testdir.makepyfile("def test_hello(): pass") - result = testdir.runpytest(p) + p = pytester.makepyfile("def test_hello(): pass") + result = pytester.runpytest(p) assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines(["*collected 0 items*"]) - def test_collectignore_exclude_on_option(self, testdir): - testdir.makeconftest( + def test_collectignore_exclude_on_option(self, pytester: Pytester) -> None: + pytester.makeconftest( """ collect_ignore = ['hello', 'test_world.py'] def pytest_addoption(parser): @@ -352,17 +372,17 @@ def pytest_configure(config): collect_ignore[:] = [] """ ) - testdir.mkdir("hello") - testdir.makepyfile(test_world="def test_hello(): pass") - result = testdir.runpytest() + pytester.mkdir("hello") + pytester.makepyfile(test_world="def test_hello(): pass") + result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.no_fnmatch_line("*passed*") - result = testdir.runpytest("--XX") + result = pytester.runpytest("--XX") assert result.ret == 0 assert "passed" in result.stdout.str() - def test_collectignoreglob_exclude_on_option(self, testdir): - testdir.makeconftest( + def test_collectignoreglob_exclude_on_option(self, pytester: Pytester) -> None: + pytester.makeconftest( """ collect_ignore_glob = ['*w*l[dt]*'] def pytest_addoption(parser): @@ -372,17 +392,17 @@ def pytest_configure(config): collect_ignore_glob[:] = [] """ ) - testdir.makepyfile(test_world="def test_hello(): pass") - testdir.makepyfile(test_welt="def test_hallo(): pass") - result = testdir.runpytest() + pytester.makepyfile(test_world="def test_hello(): pass") + pytester.makepyfile(test_welt="def test_hallo(): pass") + result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines(["*collected 0 items*"]) - result = testdir.runpytest("--XX") + result = pytester.runpytest("--XX") assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) - def test_pytest_fs_collect_hooks_are_seen(self, testdir): - testdir.makeconftest( + def test_pytest_fs_collect_hooks_are_seen(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest class MyModule(pytest.Module): @@ -392,15 +412,15 @@ def pytest_collect_file(path, parent): return MyModule.from_parent(fspath=path, parent=parent) """ ) - testdir.mkdir("sub") - testdir.makepyfile("def test_x(): pass") - result = testdir.runpytest("--co") + pytester.mkdir("sub") + pytester.makepyfile("def test_x(): pass") + result = pytester.runpytest("--co") result.stdout.fnmatch_lines(["*MyModule*", "*test_x*"]) - def test_pytest_collect_file_from_sister_dir(self, testdir): - sub1 = testdir.mkpydir("sub1") - sub2 = testdir.mkpydir("sub2") - conf1 = testdir.makeconftest( + def test_pytest_collect_file_from_sister_dir(self, pytester: Pytester) -> None: + sub1 = pytester.mkpydir("sub1") + sub2 = pytester.mkpydir("sub2") + conf1 = pytester.makeconftest( """ import pytest class MyModule1(pytest.Module): @@ -410,8 +430,8 @@ def pytest_collect_file(path, parent): return MyModule1.from_parent(fspath=path, parent=parent) """ ) - conf1.move(sub1.join(conf1.basename)) - conf2 = testdir.makeconftest( + conf1.replace(sub1.joinpath(conf1.name)) + conf2 = pytester.makeconftest( """ import pytest class MyModule2(pytest.Module): @@ -421,21 +441,21 @@ def pytest_collect_file(path, parent): return MyModule2.from_parent(fspath=path, parent=parent) """ ) - conf2.move(sub2.join(conf2.basename)) - p = testdir.makepyfile("def test_x(): pass") - p.copy(sub1.join(p.basename)) - p.copy(sub2.join(p.basename)) - result = testdir.runpytest("--co") + conf2.replace(sub2.joinpath(conf2.name)) + p = pytester.makepyfile("def test_x(): pass") + shutil.copy(p, sub1.joinpath(p.name)) + shutil.copy(p, sub2.joinpath(p.name)) + result = pytester.runpytest("--co") result.stdout.fnmatch_lines(["*MyModule1*", "*MyModule2*", "*test_x*"]) class TestSession: - def test_collect_topdir(self, testdir): - p = testdir.makepyfile("def test_func(): pass") - id = "::".join([p.basename, "test_func"]) + def test_collect_topdir(self, pytester: Pytester) -> None: + p = pytester.makepyfile("def test_func(): pass") + id = "::".join([p.name, "test_func"]) # XXX migrate to collectonly? (see below) - config = testdir.parseconfig(id) - topdir = testdir.tmpdir + config = pytester.parseconfig(id) + topdir = pytester.path rcol = Session.from_config(config) assert topdir == rcol.fspath # rootid = rcol.nodeid @@ -445,7 +465,7 @@ def test_collect_topdir(self, testdir): assert len(colitems) == 1 assert colitems[0].fspath == p - def get_reported_items(self, hookrec): + def get_reported_items(self, hookrec: HookRecorder) -> List[Item]: """Return pytest.Item instances reported by the pytest_collectreport hook""" calls = hookrec.getcalls("pytest_collectreport") return [ @@ -455,16 +475,16 @@ def get_reported_items(self, hookrec): if isinstance(x, pytest.Item) ] - def test_collect_protocol_single_function(self, testdir): - p = testdir.makepyfile("def test_func(): pass") - id = "::".join([p.basename, "test_func"]) - items, hookrec = testdir.inline_genitems(id) + def test_collect_protocol_single_function(self, pytester: Pytester) -> None: + p = pytester.makepyfile("def test_func(): pass") + id = "::".join([p.name, "test_func"]) + items, hookrec = pytester.inline_genitems(id) (item,) = items assert item.name == "test_func" newid = item.nodeid assert newid == id pprint.pprint(hookrec.calls) - topdir = testdir.tmpdir # noqa + topdir = pytester.path # noqa hookrec.assert_contains( [ ("pytest_collectstart", "collector.fspath == topdir"), @@ -478,17 +498,17 @@ def test_collect_protocol_single_function(self, testdir): # ensure we are reporting the collection of the single test item (#2464) assert [x.name for x in self.get_reported_items(hookrec)] == ["test_func"] - def test_collect_protocol_method(self, testdir): - p = testdir.makepyfile( + def test_collect_protocol_method(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ class TestClass(object): def test_method(self): pass """ ) - normid = p.basename + "::TestClass::test_method" - for id in [p.basename, p.basename + "::TestClass", normid]: - items, hookrec = testdir.inline_genitems(id) + normid = p.name + "::TestClass::test_method" + for id in [p.name, p.name + "::TestClass", normid]: + items, hookrec = pytester.inline_genitems(id) assert len(items) == 1 assert items[0].name == "test_method" newid = items[0].nodeid @@ -496,9 +516,9 @@ def test_method(self): # ensure we are reporting the collection of the single test item (#2464) assert [x.name for x in self.get_reported_items(hookrec)] == ["test_method"] - def test_collect_custom_nodes_multi_id(self, testdir): - p = testdir.makepyfile("def test_func(): pass") - testdir.makeconftest( + def test_collect_custom_nodes_multi_id(self, pytester: Pytester) -> None: + p = pytester.makepyfile("def test_func(): pass") + pytester.makeconftest( """ import pytest class SpecialItem(pytest.Item): @@ -511,11 +531,11 @@ def pytest_collect_file(path, parent): if path.basename == %r: return SpecialFile.from_parent(fspath=path, parent=parent) """ - % p.basename + % p.name ) - id = p.basename + id = p.name - items, hookrec = testdir.inline_genitems(id) + items, hookrec = pytester.inline_genitems(id) pprint.pprint(hookrec.calls) assert len(items) == 2 hookrec.assert_contains( @@ -527,18 +547,18 @@ def pytest_collect_file(path, parent): ), ("pytest_collectstart", "collector.__class__.__name__ == 'Module'"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), + ("pytest_collectreport", "report.nodeid.startswith(p.name)"), ] ) assert len(self.get_reported_items(hookrec)) == 2 - def test_collect_subdir_event_ordering(self, testdir): - p = testdir.makepyfile("def test_func(): pass") - aaa = testdir.mkpydir("aaa") - test_aaa = aaa.join("test_aaa.py") - p.move(test_aaa) + def test_collect_subdir_event_ordering(self, pytester: Pytester) -> None: + p = pytester.makepyfile("def test_func(): pass") + aaa = pytester.mkpydir("aaa") + test_aaa = aaa.joinpath("test_aaa.py") + p.replace(test_aaa) - items, hookrec = testdir.inline_genitems() + items, hookrec = pytester.inline_genitems() assert len(items) == 1 pprint.pprint(hookrec.calls) hookrec.assert_contains( @@ -549,18 +569,18 @@ def test_collect_subdir_event_ordering(self, testdir): ] ) - def test_collect_two_commandline_args(self, testdir): - p = testdir.makepyfile("def test_func(): pass") - aaa = testdir.mkpydir("aaa") - bbb = testdir.mkpydir("bbb") - test_aaa = aaa.join("test_aaa.py") - p.copy(test_aaa) - test_bbb = bbb.join("test_bbb.py") - p.move(test_bbb) + def test_collect_two_commandline_args(self, pytester: Pytester) -> None: + p = pytester.makepyfile("def test_func(): pass") + aaa = pytester.mkpydir("aaa") + bbb = pytester.mkpydir("bbb") + test_aaa = aaa.joinpath("test_aaa.py") + shutil.copy(p, test_aaa) + test_bbb = bbb.joinpath("test_bbb.py") + p.replace(test_bbb) id = "." - items, hookrec = testdir.inline_genitems(id) + items, hookrec = pytester.inline_genitems(id) assert len(items) == 2 pprint.pprint(hookrec.calls) hookrec.assert_contains( @@ -574,26 +594,26 @@ def test_collect_two_commandline_args(self, testdir): ] ) - def test_serialization_byid(self, testdir): - testdir.makepyfile("def test_func(): pass") - items, hookrec = testdir.inline_genitems() + def test_serialization_byid(self, pytester: Pytester) -> None: + pytester.makepyfile("def test_func(): pass") + items, hookrec = pytester.inline_genitems() assert len(items) == 1 (item,) = items - items2, hookrec = testdir.inline_genitems(item.nodeid) + items2, hookrec = pytester.inline_genitems(item.nodeid) (item2,) = items2 assert item2.name == item.name assert item2.fspath == item.fspath - def test_find_byid_without_instance_parents(self, testdir): - p = testdir.makepyfile( + def test_find_byid_without_instance_parents(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ class TestClass(object): def test_method(self): pass """ ) - arg = p.basename + "::TestClass::test_method" - items, hookrec = testdir.inline_genitems(arg) + arg = p.name + "::TestClass::test_method" + items, hookrec = pytester.inline_genitems(arg) assert len(items) == 1 (item,) = items assert item.nodeid.endswith("TestClass::test_method") @@ -602,43 +622,45 @@ def test_method(self): class Test_getinitialnodes: - def test_global_file(self, testdir, tmpdir) -> None: - x = tmpdir.ensure("x.py") - with tmpdir.as_cwd(): - config = testdir.parseconfigure(x) - col = testdir.getnode(config, x) + def test_global_file(self, pytester: Pytester) -> None: + tmpdir = pytester.path + x = ensure_file(tmpdir / "x.py") + with tmpdir.cwd(): + config = pytester.parseconfigure(x) + col = pytester.getnode(config, x) assert isinstance(col, pytest.Module) assert col.name == "x.py" assert col.parent is not None assert col.parent.parent is None - for col in col.listchain(): - assert col.config is config + for parent in col.listchain(): + assert parent.config is config - def test_pkgfile(self, testdir): + def test_pkgfile(self, pytester: Pytester) -> None: """Verify nesting when a module is within a package. The parent chain should match: Module -> Package -> Session. Session's parent should always be None. """ - tmpdir = testdir.tmpdir - subdir = tmpdir.join("subdir") - x = subdir.ensure("x.py") - subdir.ensure("__init__.py") - with subdir.as_cwd(): - config = testdir.parseconfigure(x) - col = testdir.getnode(config, x) + tmpdir = pytester.path + subdir = tmpdir.joinpath("subdir") + x = ensure_file(subdir / "x.py") + ensure_file(subdir / "__init__.py") + with subdir.cwd(): + config = pytester.parseconfigure(x) + col = pytester.getnode(config, x) + assert col is not None assert col.name == "x.py" assert isinstance(col, pytest.Module) assert isinstance(col.parent, pytest.Package) assert isinstance(col.parent.parent, pytest.Session) # session is batman (has no parents) assert col.parent.parent.parent is None - for col in col.listchain(): - assert col.config is config + for parent in col.listchain(): + assert parent.config is config class Test_genitems: - def test_check_collect_hashes(self, testdir): - p = testdir.makepyfile( + def test_check_collect_hashes(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def test_1(): pass @@ -647,8 +669,8 @@ def test_2(): pass """ ) - p.copy(p.dirpath(p.purebasename + "2" + ".py")) - items, reprec = testdir.inline_genitems(p.dirpath()) + shutil.copy(p, p.parent / (p.stem + "2" + ".py")) + items, reprec = pytester.inline_genitems(p.parent) assert len(items) == 4 for numi, i in enumerate(items): for numj, j in enumerate(items): @@ -656,8 +678,8 @@ def test_2(): assert hash(i) != hash(j) assert i != j - def test_example_items1(self, testdir): - p = testdir.makepyfile( + def test_example_items1(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest @@ -674,7 +696,7 @@ def testmethod_two(self, arg0): pass """ ) - items, reprec = testdir.inline_genitems(p) + items, reprec = pytester.inline_genitems(p) assert len(items) == 4 assert items[0].name == "testone" assert items[1].name == "testmethod_one" @@ -682,27 +704,27 @@ def testmethod_two(self, arg0): assert items[3].name == "testmethod_two[.[]" # let's also test getmodpath here - assert items[0].getmodpath() == "testone" - assert items[1].getmodpath() == "TestX.testmethod_one" - assert items[2].getmodpath() == "TestY.testmethod_one" + assert items[0].getmodpath() == "testone" # type: ignore[attr-defined] + assert items[1].getmodpath() == "TestX.testmethod_one" # type: ignore[attr-defined] + assert items[2].getmodpath() == "TestY.testmethod_one" # type: ignore[attr-defined] # PR #6202: Fix incorrect result of getmodpath method. (Resolves issue #6189) - assert items[3].getmodpath() == "TestY.testmethod_two[.[]" + assert items[3].getmodpath() == "TestY.testmethod_two[.[]" # type: ignore[attr-defined] - s = items[0].getmodpath(stopatmodule=False) + s = items[0].getmodpath(stopatmodule=False) # type: ignore[attr-defined] assert s.endswith("test_example_items1.testone") print(s) - def test_class_and_functions_discovery_using_glob(self, testdir): + def test_class_and_functions_discovery_using_glob(self, pytester: Pytester) -> None: """Test that Python_classes and Python_functions config options work as prefixes and glob-like patterns (#600).""" - testdir.makeini( + pytester.makeini( """ [pytest] python_classes = *Suite Test python_functions = *_test test """ ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ class MyTestSuite(object): def x_test(self): @@ -713,13 +735,13 @@ def test_y(self): pass """ ) - items, reprec = testdir.inline_genitems(p) - ids = [x.getmodpath() for x in items] + items, reprec = pytester.inline_genitems(p) + ids = [x.getmodpath() for x in items] # type: ignore[attr-defined] assert ids == ["MyTestSuite.x_test", "TestCase.test_y"] -def test_matchnodes_two_collections_same_file(testdir): - testdir.makeconftest( +def test_matchnodes_two_collections_same_file(pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest def pytest_configure(config): @@ -751,17 +773,17 @@ def runtest(self): pass """ ) - p = testdir.makefile(".abc", "") - result = testdir.runpytest() + p = pytester.makefile(".abc", "") + result = pytester.runpytest() assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) - res = testdir.runpytest("%s::item2" % p.basename) + res = pytester.runpytest("%s::item2" % p.name) res.stdout.fnmatch_lines(["*1 passed*"]) class TestNodekeywords: - def test_no_under(self, testdir): - modcol = testdir.getmodulecol( + def test_no_under(self, pytester: Pytester) -> None: + modcol = pytester.getmodulecol( """ def test_pass(): pass def test_fail(): assert 0 @@ -773,8 +795,8 @@ def test_fail(): assert 0 assert not x.startswith("_") assert modcol.name in repr(modcol.keywords) - def test_issue345(self, testdir): - testdir.makepyfile( + def test_issue345(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_should_not_be_selected(): assert False, 'I should not have been selected to run' @@ -783,17 +805,19 @@ def test___repr__(): pass """ ) - reprec = testdir.inline_run("-k repr") + reprec = pytester.inline_run("-k repr") reprec.assertoutcome(passed=1, failed=0) - def test_keyword_matching_is_case_insensitive_by_default(self, testdir): + def test_keyword_matching_is_case_insensitive_by_default( + self, pytester: Pytester + ) -> None: """Check that selection via -k EXPRESSION is case-insensitive. Since markers are also added to the node keywords, they too can be matched without having to think about case sensitivity. """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -820,7 +844,7 @@ def test_failing_5(): ) num_matching_tests = 4 for expression in ("specifictopic", "SPECIFICTOPIC", "SpecificTopic"): - reprec = testdir.inline_run("-k " + expression) + reprec = pytester.inline_run("-k " + expression) reprec.assertoutcome(passed=num_matching_tests, failed=0) @@ -846,11 +870,11 @@ def test_4(): ) -def test_exit_on_collection_error(testdir): +def test_exit_on_collection_error(pytester: Pytester) -> None: """Verify that all collection errors are collected and no tests executed""" - testdir.makepyfile(**COLLECTION_ERROR_PY_FILES) + pytester.makepyfile(**COLLECTION_ERROR_PY_FILES) - res = testdir.runpytest() + res = pytester.runpytest() assert res.ret == 2 res.stdout.fnmatch_lines( @@ -864,14 +888,16 @@ def test_exit_on_collection_error(testdir): ) -def test_exit_on_collection_with_maxfail_smaller_than_n_errors(testdir): +def test_exit_on_collection_with_maxfail_smaller_than_n_errors( + pytester: Pytester, +) -> None: """ Verify collection is aborted once maxfail errors are encountered ignoring further modules which would cause more collection errors. """ - testdir.makepyfile(**COLLECTION_ERROR_PY_FILES) + pytester.makepyfile(**COLLECTION_ERROR_PY_FILES) - res = testdir.runpytest("--maxfail=1") + res = pytester.runpytest("--maxfail=1") assert res.ret == 1 res.stdout.fnmatch_lines( [ @@ -885,14 +911,16 @@ def test_exit_on_collection_with_maxfail_smaller_than_n_errors(testdir): res.stdout.no_fnmatch_line("*test_03*") -def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir): +def test_exit_on_collection_with_maxfail_bigger_than_n_errors( + pytester: Pytester, +) -> None: """ Verify the test run aborts due to collection errors even if maxfail count of errors was not reached. """ - testdir.makepyfile(**COLLECTION_ERROR_PY_FILES) + pytester.makepyfile(**COLLECTION_ERROR_PY_FILES) - res = testdir.runpytest("--maxfail=4") + res = pytester.runpytest("--maxfail=4") assert res.ret == 2 res.stdout.fnmatch_lines( [ @@ -907,14 +935,14 @@ def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir): ) -def test_continue_on_collection_errors(testdir): +def test_continue_on_collection_errors(pytester: Pytester) -> None: """ Verify tests are executed even when collection errors occur when the --continue-on-collection-errors flag is set """ - testdir.makepyfile(**COLLECTION_ERROR_PY_FILES) + pytester.makepyfile(**COLLECTION_ERROR_PY_FILES) - res = testdir.runpytest("--continue-on-collection-errors") + res = pytester.runpytest("--continue-on-collection-errors") assert res.ret == 1 res.stdout.fnmatch_lines( @@ -922,7 +950,7 @@ def test_continue_on_collection_errors(testdir): ) -def test_continue_on_collection_errors_maxfail(testdir): +def test_continue_on_collection_errors_maxfail(pytester: Pytester) -> None: """ Verify tests are executed even when collection errors occur and that maxfail is honoured (including the collection error count). @@ -930,18 +958,18 @@ def test_continue_on_collection_errors_maxfail(testdir): test_4 is never executed because the test run is with --maxfail=3 which means it is interrupted after the 2 collection errors + 1 failure. """ - testdir.makepyfile(**COLLECTION_ERROR_PY_FILES) + pytester.makepyfile(**COLLECTION_ERROR_PY_FILES) - res = testdir.runpytest("--continue-on-collection-errors", "--maxfail=3") + res = pytester.runpytest("--continue-on-collection-errors", "--maxfail=3") assert res.ret == 1 res.stdout.fnmatch_lines(["collected 2 items / 2 errors", "*1 failed, 2 errors*"]) -def test_fixture_scope_sibling_conftests(testdir): +def test_fixture_scope_sibling_conftests(pytester: Pytester) -> None: """Regression test case for https://github.com/pytest-dev/pytest/issues/2836""" - foo_path = testdir.mkdir("foo") - foo_path.join("conftest.py").write( + foo_path = pytester.mkdir("foo") + foo_path.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -951,13 +979,13 @@ def fix(): """ ) ) - foo_path.join("test_foo.py").write("def test_foo(fix): assert fix == 1") + foo_path.joinpath("test_foo.py").write_text("def test_foo(fix): assert fix == 1") # Tests in `food/` should not see the conftest fixture from `foo/` - food_path = testdir.mkpydir("food") - food_path.join("test_food.py").write("def test_food(fix): assert fix == 1") + food_path = pytester.mkpydir("food") + food_path.joinpath("test_food.py").write_text("def test_food(fix): assert fix == 1") - res = testdir.runpytest() + res = pytester.runpytest() assert res.ret == 1 res.stdout.fnmatch_lines( @@ -969,10 +997,10 @@ def fix(): ) -def test_collect_init_tests(testdir): +def test_collect_init_tests(pytester: Pytester) -> None: """Check that we collect files from __init__.py files when they patch the 'python_files' (#3773)""" - p = testdir.copy_example("collect/collect_init_tests") - result = testdir.runpytest(p, "--collect-only") + p = pytester.copy_example("collect/collect_init_tests") + result = pytester.runpytest(p, "--collect-only") result.stdout.fnmatch_lines( [ "collected 2 items", @@ -983,7 +1011,7 @@ def test_collect_init_tests(testdir): " ", ] ) - result = testdir.runpytest("./tests", "--collect-only") + result = pytester.runpytest("./tests", "--collect-only") result.stdout.fnmatch_lines( [ "collected 2 items", @@ -995,7 +1023,7 @@ def test_collect_init_tests(testdir): ] ) # Ignores duplicates with "." and pkginit (#4310). - result = testdir.runpytest("./tests", ".", "--collect-only") + result = pytester.runpytest("./tests", ".", "--collect-only") result.stdout.fnmatch_lines( [ "collected 2 items", @@ -1007,7 +1035,7 @@ def test_collect_init_tests(testdir): ] ) # Same as before, but different order. - result = testdir.runpytest(".", "tests", "--collect-only") + result = pytester.runpytest(".", "tests", "--collect-only") result.stdout.fnmatch_lines( [ "collected 2 items", @@ -1018,23 +1046,23 @@ def test_collect_init_tests(testdir): " ", ] ) - result = testdir.runpytest("./tests/test_foo.py", "--collect-only") + result = pytester.runpytest("./tests/test_foo.py", "--collect-only") result.stdout.fnmatch_lines( ["", " ", " "] ) result.stdout.no_fnmatch_line("*test_init*") - result = testdir.runpytest("./tests/__init__.py", "--collect-only") + result = pytester.runpytest("./tests/__init__.py", "--collect-only") result.stdout.fnmatch_lines( ["", " ", " "] ) result.stdout.no_fnmatch_line("*test_foo*") -def test_collect_invalid_signature_message(testdir): +def test_collect_invalid_signature_message(pytester: Pytester) -> None: """Check that we issue a proper message when we can't determine the signature of a test function (#4026). """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1044,17 +1072,17 @@ def fix(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( ["Could not determine arguments of *.fix *: invalid method signature"] ) -def test_collect_handles_raising_on_dunder_class(testdir): +def test_collect_handles_raising_on_dunder_class(pytester: Pytester) -> None: """Handle proxy classes like Django's LazySettings that might raise on ``isinstance`` (#4266). """ - testdir.makepyfile( + pytester.makepyfile( """ class ImproperlyConfigured(Exception): pass @@ -1072,14 +1100,14 @@ def test_1(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed in*"]) assert result.ret == 0 -def test_collect_with_chdir_during_import(testdir): - subdir = testdir.tmpdir.mkdir("sub") - testdir.tmpdir.join("conftest.py").write( +def test_collect_with_chdir_during_import(pytester: Pytester) -> None: + subdir = pytester.mkdir("sub") + pytester.path.joinpath("conftest.py").write_text( textwrap.dedent( """ import os @@ -1088,7 +1116,7 @@ def test_collect_with_chdir_during_import(testdir): % (str(subdir),) ) ) - testdir.makepyfile( + pytester.makepyfile( """ def test_1(): import os @@ -1096,31 +1124,33 @@ def test_1(): """ % (str(subdir),) ) - with testdir.tmpdir.as_cwd(): - result = testdir.runpytest() + with pytester.path.cwd(): + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed in*"]) assert result.ret == 0 # Handles relative testpaths. - testdir.makeini( + pytester.makeini( """ [pytest] testpaths = . """ ) - with testdir.tmpdir.as_cwd(): - result = testdir.runpytest("--collect-only") + with pytester.path.cwd(): + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["collected 1 item"]) -def test_collect_pyargs_with_testpaths(testdir, monkeypatch): - testmod = testdir.mkdir("testmod") +def test_collect_pyargs_with_testpaths( + pytester: Pytester, monkeypatch: MonkeyPatch +) -> None: + testmod = pytester.mkdir("testmod") # NOTE: __init__.py is not collected since it does not match python_files. - testmod.ensure("__init__.py").write("def test_func(): pass") - testmod.ensure("test_file.py").write("def test_func(): pass") + testmod.joinpath("__init__.py").write_text("def test_func(): pass") + testmod.joinpath("test_file.py").write_text("def test_func(): pass") - root = testdir.mkdir("root") - root.ensure("pytest.ini").write( + root = pytester.mkdir("root") + root.joinpath("pytest.ini").write_text( textwrap.dedent( """ [pytest] @@ -1129,32 +1159,32 @@ def test_collect_pyargs_with_testpaths(testdir, monkeypatch): """ ) ) - monkeypatch.setenv("PYTHONPATH", str(testdir.tmpdir), prepend=os.pathsep) - with root.as_cwd(): - result = testdir.runpytest_subprocess() + monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep) + with root.cwd(): + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 passed in*"]) -def test_collect_symlink_file_arg(testdir): +def test_collect_symlink_file_arg(pytester: Pytester) -> None: """Collect a direct symlink works even if it does not match python_files (#4325).""" - real = testdir.makepyfile( + real = pytester.makepyfile( real=""" def test_nodeid(request): assert request.node.nodeid == "symlink.py::test_nodeid" """ ) - symlink = testdir.tmpdir.join("symlink.py") + symlink = pytester.path.joinpath("symlink.py") symlink_or_skip(real, symlink) - result = testdir.runpytest("-v", symlink) + result = pytester.runpytest("-v", symlink) result.stdout.fnmatch_lines(["symlink.py::test_nodeid PASSED*", "*1 passed in*"]) assert result.ret == 0 -def test_collect_symlink_out_of_tree(testdir): +def test_collect_symlink_out_of_tree(pytester: Pytester) -> None: """Test collection of symlink via out-of-tree rootdir.""" - sub = testdir.tmpdir.join("sub") - real = sub.join("test_real.py") - real.write( + sub = pytester.mkdir("sub") + real = sub.joinpath("test_real.py") + real.write_text( textwrap.dedent( """ def test_nodeid(request): @@ -1162,14 +1192,13 @@ def test_nodeid(request): assert request.node.nodeid == "test_real.py::test_nodeid" """ ), - ensure=True, ) - out_of_tree = testdir.tmpdir.join("out_of_tree").ensure(dir=True) - symlink_to_sub = out_of_tree.join("symlink_to_sub") + out_of_tree = pytester.mkdir("out_of_tree") + symlink_to_sub = out_of_tree.joinpath("symlink_to_sub") symlink_or_skip(sub, symlink_to_sub) - sub.chdir() - result = testdir.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub) + os.chdir(sub) + result = pytester.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub) result.stdout.fnmatch_lines( [ # Should not contain "sub/"! @@ -1188,30 +1217,31 @@ def test_collect_symlink_dir(pytester: Pytester) -> None: result.assert_outcomes(passed=2) -def test_collectignore_via_conftest(testdir): +def test_collectignore_via_conftest(pytester: Pytester) -> None: """collect_ignore in parent conftest skips importing child (issue #4592).""" - tests = testdir.mkpydir("tests") - tests.ensure("conftest.py").write("collect_ignore = ['ignore_me']") + tests = pytester.mkpydir("tests") + tests.joinpath("conftest.py").write_text("collect_ignore = ['ignore_me']") - ignore_me = tests.mkdir("ignore_me") - ignore_me.ensure("__init__.py") - ignore_me.ensure("conftest.py").write("assert 0, 'should_not_be_called'") + ignore_me = tests.joinpath("ignore_me") + ignore_me.mkdir() + ignore_me.joinpath("__init__.py").touch() + ignore_me.joinpath("conftest.py").write_text("assert 0, 'should_not_be_called'") - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED -def test_collect_pkg_init_and_file_in_args(testdir): - subdir = testdir.mkdir("sub") - init = subdir.ensure("__init__.py") - init.write("def test_init(): pass") - p = subdir.ensure("test_file.py") - p.write("def test_file(): pass") +def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None: + subdir = pytester.mkdir("sub") + init = subdir.joinpath("__init__.py") + init.write_text("def test_init(): pass") + p = subdir.joinpath("test_file.py") + p.write_text("def test_file(): pass") # NOTE: without "-o python_files=*.py" this collects test_file.py twice. # This changed/broke with "Add package scoped fixtures #2283" (2b1410895) # initially (causing a RecursionError). - result = testdir.runpytest("-v", str(init), str(p)) + result = pytester.runpytest("-v", str(init), str(p)) result.stdout.fnmatch_lines( [ "sub/test_file.py::test_file PASSED*", @@ -1220,7 +1250,7 @@ def test_collect_pkg_init_and_file_in_args(testdir): ] ) - result = testdir.runpytest("-v", "-o", "python_files=*.py", str(init), str(p)) + result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init), str(p)) result.stdout.fnmatch_lines( [ "sub/__init__.py::test_init PASSED*", @@ -1230,33 +1260,33 @@ def test_collect_pkg_init_and_file_in_args(testdir): ) -def test_collect_pkg_init_only(testdir): - subdir = testdir.mkdir("sub") - init = subdir.ensure("__init__.py") - init.write("def test_init(): pass") +def test_collect_pkg_init_only(pytester: Pytester) -> None: + subdir = pytester.mkdir("sub") + init = subdir.joinpath("__init__.py") + init.write_text("def test_init(): pass") - result = testdir.runpytest(str(init)) + result = pytester.runpytest(str(init)) result.stdout.fnmatch_lines(["*no tests ran in*"]) - result = testdir.runpytest("-v", "-o", "python_files=*.py", str(init)) + result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init)) result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) @pytest.mark.parametrize("use_pkg", (True, False)) -def test_collect_sub_with_symlinks(use_pkg, testdir): +def test_collect_sub_with_symlinks(use_pkg: bool, pytester: Pytester) -> None: """Collection works with symlinked files and broken symlinks""" - sub = testdir.mkdir("sub") + sub = pytester.mkdir("sub") if use_pkg: - sub.ensure("__init__.py") - sub.join("test_file.py").write("def test_file(): pass") + sub.joinpath("__init__.py").touch() + sub.joinpath("test_file.py").write_text("def test_file(): pass") # Create a broken symlink. - symlink_or_skip("test_doesnotexist.py", sub.join("test_broken.py")) + symlink_or_skip("test_doesnotexist.py", sub.joinpath("test_broken.py")) # Symlink that gets collected. - symlink_or_skip("test_file.py", sub.join("test_symlink.py")) + symlink_or_skip("test_file.py", sub.joinpath("test_symlink.py")) - result = testdir.runpytest("-v", str(sub)) + result = pytester.runpytest("-v", str(sub)) result.stdout.fnmatch_lines( [ "sub/test_file.py::test_file PASSED*", @@ -1266,9 +1296,9 @@ def test_collect_sub_with_symlinks(use_pkg, testdir): ) -def test_collector_respects_tbstyle(testdir): - p1 = testdir.makepyfile("assert 0") - result = testdir.runpytest(p1, "--tb=native") +def test_collector_respects_tbstyle(pytester: Pytester) -> None: + p1 = pytester.makepyfile("assert 0") + result = pytester.runpytest(p1, "--tb=native") assert result.ret == ExitCode.INTERRUPTED result.stdout.fnmatch_lines( [ @@ -1283,28 +1313,28 @@ def test_collector_respects_tbstyle(testdir): ) -def test_does_not_eagerly_collect_packages(testdir): - testdir.makepyfile("def test(): pass") - pydir = testdir.mkpydir("foopkg") - pydir.join("__init__.py").write("assert False") - result = testdir.runpytest() +def test_does_not_eagerly_collect_packages(pytester: Pytester) -> None: + pytester.makepyfile("def test(): pass") + pydir = pytester.mkpydir("foopkg") + pydir.joinpath("__init__.py").write_text("assert False") + result = pytester.runpytest() assert result.ret == ExitCode.OK -def test_does_not_put_src_on_path(testdir): +def test_does_not_put_src_on_path(pytester: Pytester) -> None: # `src` is not on sys.path so it should not be importable - testdir.tmpdir.join("src/nope/__init__.py").ensure() - testdir.makepyfile( + ensure_file(pytester.path / "src/nope/__init__.py") + pytester.makepyfile( "import pytest\n" "def test():\n" " with pytest.raises(ImportError):\n" " import nope\n" ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == ExitCode.OK -def test_fscollector_from_parent(tmpdir, request): +def test_fscollector_from_parent(testdir: Testdir, request: FixtureRequest) -> None: """Ensure File.from_parent can forward custom arguments to the constructor. Context: https://github.com/pytest-dev/pytest-cpp/pull/47 @@ -1320,21 +1350,21 @@ def from_parent(cls, parent, *, fspath, x): return super().from_parent(parent=parent, fspath=fspath, x=x) collector = MyCollector.from_parent( - parent=request.session, fspath=tmpdir / "foo", x=10 + parent=request.session, fspath=testdir.tmpdir / "foo", x=10 ) assert collector.x == 10 class TestImportModeImportlib: - def test_collect_duplicate_names(self, testdir): + def test_collect_duplicate_names(self, pytester: Pytester) -> None: """--import-mode=importlib can import modules with same names that are not in packages.""" - testdir.makepyfile( + pytester.makepyfile( **{ "tests_a/test_foo.py": "def test_foo1(): pass", "tests_b/test_foo.py": "def test_foo2(): pass", } ) - result = testdir.runpytest("-v", "--import-mode=importlib") + result = pytester.runpytest("-v", "--import-mode=importlib") result.stdout.fnmatch_lines( [ "tests_a/test_foo.py::test_foo1 *", @@ -1343,11 +1373,11 @@ def test_collect_duplicate_names(self, testdir): ] ) - def test_conftest(self, testdir): + def test_conftest(self, pytester: Pytester) -> None: """Directory containing conftest modules are not put in sys.path as a side-effect of importing them.""" - tests_dir = testdir.tmpdir.join("tests") - testdir.makepyfile( + tests_dir = pytester.path.joinpath("tests") + pytester.makepyfile( **{ "tests/conftest.py": "", "tests/test_foo.py": """ @@ -1359,13 +1389,13 @@ def test_check(): ), } ) - result = testdir.runpytest("-v", "--import-mode=importlib") + result = pytester.runpytest("-v", "--import-mode=importlib") result.stdout.fnmatch_lines(["* 1 passed in *"]) - def setup_conftest_and_foo(self, testdir): + def setup_conftest_and_foo(self, pytester: Pytester) -> None: """Setup a tests folder to be used to test if modules in that folder can be imported due to side-effects of --import-mode or not.""" - testdir.makepyfile( + pytester.makepyfile( **{ "tests/conftest.py": "", "tests/foo.py": """ @@ -1379,20 +1409,20 @@ def test_check(): } ) - def test_modules_importable_as_side_effect(self, testdir): + def test_modules_importable_as_side_effect(self, pytester: Pytester) -> None: """In import-modes `prepend` and `append`, we are able to import modules from folders containing conftest.py files due to the side effect of changing sys.path.""" - self.setup_conftest_and_foo(testdir) - result = testdir.runpytest("-v", "--import-mode=prepend") + self.setup_conftest_and_foo(pytester) + result = pytester.runpytest("-v", "--import-mode=prepend") result.stdout.fnmatch_lines(["* 1 passed in *"]) - def test_modules_not_importable_as_side_effect(self, testdir): + def test_modules_not_importable_as_side_effect(self, pytester: Pytester) -> None: """In import-mode `importlib`, modules in folders containing conftest.py are not importable, as don't change sys.path or sys.modules as side effect of importing the conftest.py file. """ - self.setup_conftest_and_foo(testdir) - result = testdir.runpytest("-v", "--import-mode=importlib") + self.setup_conftest_and_foo(pytester) + result = pytester.runpytest("-v", "--import-mode=importlib") result.stdout.fnmatch_lines( [ "*ModuleNotFoundError: No module named 'foo'", @@ -1402,29 +1432,29 @@ def test_modules_not_importable_as_side_effect(self, testdir): ) -def test_does_not_crash_on_error_from_decorated_function(testdir: Testdir) -> None: +def test_does_not_crash_on_error_from_decorated_function(pytester: Pytester) -> None: """Regression test for an issue around bad exception formatting due to assertion rewriting mangling lineno's (#4984).""" - testdir.makepyfile( + pytester.makepyfile( """ @pytest.fixture def a(): return 4 """ ) - result = testdir.runpytest() + result = pytester.runpytest() # Not INTERNAL_ERROR assert result.ret == ExitCode.INTERRUPTED -def test_does_not_crash_on_recursive_symlink(testdir: Testdir) -> None: +def test_does_not_crash_on_recursive_symlink(pytester: Pytester) -> None: """Regression test for an issue around recursive symlinks (#7951).""" - symlink_or_skip("recursive", testdir.tmpdir.join("recursive")) - testdir.makepyfile( + symlink_or_skip("recursive", pytester.path.joinpath("recursive")) + pytester.makepyfile( """ def test_foo(): assert True """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == ExitCode.OK assert result.parseoutcomes() == {"passed": 1} From 4c0513bc18f52b24b69d60c0b1b5e9666e084c06 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 31 Oct 2020 22:44:10 +0200 Subject: [PATCH 764/823] fixtures: deprecate pytest.yield_fixture() --- changelog/7988.deprecation.rst | 3 ++ doc/en/deprecations.rst | 9 ++++++ src/_pytest/deprecated.py | 4 +++ src/_pytest/fixtures.py | 2 ++ testing/deprecated_test.py | 8 +++++ testing/python/fixtures.py | 54 ++++++++++++---------------------- testing/test_unittest.py | 13 ++++---- 7 files changed, 51 insertions(+), 42 deletions(-) create mode 100644 changelog/7988.deprecation.rst diff --git a/changelog/7988.deprecation.rst b/changelog/7988.deprecation.rst new file mode 100644 index 00000000000..34f646c9ab4 --- /dev/null +++ b/changelog/7988.deprecation.rst @@ -0,0 +1,3 @@ +The ``@pytest.yield_fixture`` decorator/function is now deprecated. Use :func:`pytest.fixture` instead. + +``yield_fixture`` has been an alias for ``fixture`` for a very long time, so can be search/replaced safely. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index d588b1bea8a..5ef1053e0b4 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -31,6 +31,15 @@ flag for all strictness related options (``--strict-markers`` and ``--strict-con at the moment, more might be introduced in the future). +The ``yield_fixture`` function/decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.2 + +``pytest.yield_fixture`` is a deprecated alias for :func:`pytest.fixture`. + +It has been so for a very long time, so can be search/replaced safely. + The ``pytest_warning_captured`` hook ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index a9a162f41fd..2e9154e8380 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -32,6 +32,10 @@ "Please update to the new name.", ) +YIELD_FIXTURE = PytestDeprecationWarning( + "@pytest.yield_fixture is deprecated.\n" + "Use @pytest.fixture instead; they are the same." +) MINUS_K_DASH = PytestDeprecationWarning( "The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead." diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 18094f21c3b..cef998c03b7 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -50,6 +50,7 @@ from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.deprecated import FILLFUNCARGS +from _pytest.deprecated import YIELD_FIXTURE from _pytest.mark import Mark from _pytest.mark import ParameterSet from _pytest.mark.structures import MarkDecorator @@ -1339,6 +1340,7 @@ def yield_fixture( .. deprecated:: 3.0 Use :py:func:`pytest.fixture` directly instead. """ + warnings.warn(YIELD_FIXTURE, stacklevel=2) return fixture( fixture_function, *args, diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index e0bc1db70a0..0d1b58ad16a 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -115,3 +115,11 @@ def test_foo(): pass "*PytestDeprecationWarning: The --strict option is deprecated, use --strict-markers instead.", ] ) + + +def test_yield_fixture_is_deprecated() -> None: + with pytest.warns(DeprecationWarning, match=r"yield_fixture is deprecated"): + + @pytest.yield_fixture + def fix(): + assert False diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index a4838ee5167..a5637b47642 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -8,6 +8,7 @@ from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest from _pytest.pytester import get_public_names +from _pytest.pytester import Testdir def test_getfuncargnames_functions(): @@ -3526,28 +3527,11 @@ def foo(): class TestContextManagerFixtureFuncs: - @pytest.fixture(params=["fixture", "yield_fixture"]) - def flavor(self, request, testdir, monkeypatch): - monkeypatch.setenv("PYTEST_FIXTURE_FLAVOR", request.param) - testdir.makepyfile( - test_context=""" - import os - import pytest - import warnings - VAR = "PYTEST_FIXTURE_FLAVOR" - if VAR not in os.environ: - warnings.warn("PYTEST_FIXTURE_FLAVOR was not set, assuming fixture") - fixture = pytest.fixture - else: - fixture = getattr(pytest, os.environ[VAR]) - """ - ) - - def test_simple(self, testdir, flavor): + def test_simple(self, testdir: Testdir) -> None: testdir.makepyfile( """ - from test_context import fixture - @fixture + import pytest + @pytest.fixture def arg1(): print("setup") yield 1 @@ -3571,11 +3555,11 @@ def test_2(arg1): """ ) - def test_scoped(self, testdir, flavor): + def test_scoped(self, testdir: Testdir) -> None: testdir.makepyfile( """ - from test_context import fixture - @fixture(scope="module") + import pytest + @pytest.fixture(scope="module") def arg1(): print("setup") yield 1 @@ -3596,11 +3580,11 @@ def test_2(arg1): """ ) - def test_setup_exception(self, testdir, flavor): + def test_setup_exception(self, testdir: Testdir) -> None: testdir.makepyfile( """ - from test_context import fixture - @fixture(scope="module") + import pytest + @pytest.fixture(scope="module") def arg1(): pytest.fail("setup") yield 1 @@ -3616,11 +3600,11 @@ def test_1(arg1): """ ) - def test_teardown_exception(self, testdir, flavor): + def test_teardown_exception(self, testdir: Testdir) -> None: testdir.makepyfile( """ - from test_context import fixture - @fixture(scope="module") + import pytest + @pytest.fixture(scope="module") def arg1(): yield 1 pytest.fail("teardown") @@ -3636,11 +3620,11 @@ def test_1(arg1): """ ) - def test_yields_more_than_one(self, testdir, flavor): + def test_yields_more_than_one(self, testdir: Testdir) -> None: testdir.makepyfile( """ - from test_context import fixture - @fixture(scope="module") + import pytest + @pytest.fixture(scope="module") def arg1(): yield 1 yield 2 @@ -3656,11 +3640,11 @@ def test_1(arg1): """ ) - def test_custom_name(self, testdir, flavor): + def test_custom_name(self, testdir: Testdir) -> None: testdir.makepyfile( """ - from test_context import fixture - @fixture(name='meow') + import pytest + @pytest.fixture(name='meow') def arg1(): return 'mew' def test_1(meow): diff --git a/testing/test_unittest.py b/testing/test_unittest.py index f6c8c48eddc..2c8d03cb981 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -4,6 +4,7 @@ import pytest from _pytest.config import ExitCode +from _pytest.pytester import Testdir def test_simple_unittest(testdir): @@ -781,20 +782,18 @@ def test_passing_test_is_fail(self): assert result.ret == 1 -@pytest.mark.parametrize( - "fix_type, stmt", [("fixture", "return"), ("yield_fixture", "yield")] -) -def test_unittest_setup_interaction(testdir, fix_type, stmt): +@pytest.mark.parametrize("stmt", ["return", "yield"]) +def test_unittest_setup_interaction(testdir: Testdir, stmt: str) -> None: testdir.makepyfile( """ import unittest import pytest class MyTestCase(unittest.TestCase): - @pytest.{fix_type}(scope="class", autouse=True) + @pytest.fixture(scope="class", autouse=True) def perclass(self, request): request.cls.hello = "world" {stmt} - @pytest.{fix_type}(scope="function", autouse=True) + @pytest.fixture(scope="function", autouse=True) def perfunction(self, request): request.instance.funcname = request.function.__name__ {stmt} @@ -809,7 +808,7 @@ def test_method2(self): def test_classattr(self): assert self.__class__.hello == "world" """.format( - fix_type=fix_type, stmt=stmt + stmt=stmt ) ) result = testdir.runpytest() From 1cbb0c3554e593abfe4633fdbe0a3d554f335d0c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 6 Nov 2020 19:25:40 +0200 Subject: [PATCH 765/823] Stop importing `pytest` to avoid upcoming import cycles Don't import `pytest` from within some `_pytest` modules since an upcoming commit will import from them into `pytest`. It would have been nice not to have to do it, so that internal plugins look more like external plugins, but with the existing layout this seems unavoidable. --- src/_pytest/cacheprovider.py | 15 +++++++------ src/_pytest/capture.py | 28 +++++++++++++----------- src/_pytest/logging.py | 32 ++++++++++++++------------- src/_pytest/monkeypatch.py | 4 ++-- src/_pytest/pytester.py | 42 ++++++++++++++++++++---------------- src/_pytest/terminal.py | 15 +++++++------ src/_pytest/tmpdir.py | 10 ++++----- 7 files changed, 79 insertions(+), 67 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 09f3d6653fe..1689b9a410f 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -15,7 +15,6 @@ import attr import py -import pytest from .pathlib import resolve_from_str from .pathlib import rm_rf from .reports import CollectReport @@ -24,7 +23,9 @@ from _pytest.compat import final from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.config import hookimpl from _pytest.config.argparsing import Parser +from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.python import Module @@ -182,7 +183,7 @@ def __init__(self, lfplugin: "LFPlugin") -> None: self.lfplugin = lfplugin self._collected_at_least_one_failure = False - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_make_collect_report(self, collector: nodes.Collector): if isinstance(collector, Session): out = yield @@ -229,7 +230,7 @@ class LFPluginCollSkipfiles: def __init__(self, lfplugin: "LFPlugin") -> None: self.lfplugin = lfplugin - @pytest.hookimpl + @hookimpl def pytest_make_collect_report( self, collector: nodes.Collector ) -> Optional[CollectReport]: @@ -291,7 +292,7 @@ def pytest_collectreport(self, report: CollectReport) -> None: else: self.lastfailed[report.nodeid] = True - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, config: Config, items: List[nodes.Item] ) -> Generator[None, None, None]: @@ -363,7 +364,7 @@ def __init__(self, config: Config) -> None: assert config.cache is not None self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, items: List[nodes.Item] ) -> Generator[None, None, None]: @@ -466,14 +467,14 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: return None -@pytest.hookimpl(tryfirst=True) +@hookimpl(tryfirst=True) def pytest_configure(config: Config) -> None: config.cache = Cache.for_config(config) config.pluginmanager.register(LFPlugin(config), "lfplugin") config.pluginmanager.register(NFPlugin(config), "nfplugin") -@pytest.fixture +@fixture def cache(request: FixtureRequest) -> Cache: """Return a cache object that can persist state between testing sessions. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 25535f67b75..1c3a2b81959 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -17,12 +17,14 @@ from typing import TYPE_CHECKING from typing import Union -import pytest from _pytest.compat import final from _pytest.config import Config +from _pytest.config import hookimpl from _pytest.config.argparsing import Parser +from _pytest.fixtures import fixture from _pytest.fixtures import SubRequest from _pytest.nodes import Collector +from _pytest.nodes import File from _pytest.nodes import Item if TYPE_CHECKING: @@ -145,7 +147,7 @@ def _reopen_stdio(f, mode): sys.stderr = _reopen_stdio(sys.stderr, "wb") -@pytest.hookimpl(hookwrapper=True) +@hookimpl(hookwrapper=True) def pytest_load_initial_conftests(early_config: Config): ns = early_config.known_args_namespace if ns.capture == "fd": @@ -784,9 +786,9 @@ def item_capture(self, when: str, item: Item) -> Generator[None, None, None]: # Hooks - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_make_collect_report(self, collector: Collector): - if isinstance(collector, pytest.File): + if isinstance(collector, File): self.resume_global_capture() outcome = yield self.suspend_global_capture() @@ -799,26 +801,26 @@ def pytest_make_collect_report(self, collector: Collector): else: yield - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: with self.item_capture("setup", item): yield - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]: with self.item_capture("call", item): yield - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]: with self.item_capture("teardown", item): yield - @pytest.hookimpl(tryfirst=True) + @hookimpl(tryfirst=True) def pytest_keyboard_interrupt(self) -> None: self.stop_global_capturing() - @pytest.hookimpl(tryfirst=True) + @hookimpl(tryfirst=True) def pytest_internalerror(self) -> None: self.stop_global_capturing() @@ -893,7 +895,7 @@ def disabled(self) -> Generator[None, None, None]: # The fixtures. -@pytest.fixture +@fixture def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. @@ -910,7 +912,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: capman.unset_fixture() -@pytest.fixture +@fixture def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. @@ -927,7 +929,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, capman.unset_fixture() -@pytest.fixture +@fixture def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: """Enable text capturing of writes to file descriptors ``1`` and ``2``. @@ -944,7 +946,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: capman.unset_fixture() -@pytest.fixture +@fixture def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: """Enable bytes capturing of writes to file descriptors ``1`` and ``2``. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 3b046c95441..2f5da8e7a00 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -16,7 +16,6 @@ from typing import TypeVar from typing import Union -import pytest from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.capture import CaptureManager @@ -25,7 +24,10 @@ from _pytest.config import _strtobool from _pytest.config import Config from _pytest.config import create_terminal_writer +from _pytest.config import hookimpl +from _pytest.config import UsageError from _pytest.config.argparsing import Parser +from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.store import StoreKey @@ -468,7 +470,7 @@ def at_level( self.handler.setLevel(handler_orig_level) -@pytest.fixture +@fixture def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]: """Access and control log capturing. @@ -501,7 +503,7 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i return int(getattr(logging, log_level, log_level)) except ValueError as e: # Python logging does not recognise this as a logging level - raise pytest.UsageError( + raise UsageError( "'{}' is not recognized as a logging level name for " "'{}'. Please consider passing the " "logging level num instead.".format(log_level, setting_name) @@ -509,7 +511,7 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i # run after terminalreporter/capturemanager are configured -@pytest.hookimpl(trylast=True) +@hookimpl(trylast=True) def pytest_configure(config: Config) -> None: config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") @@ -639,7 +641,7 @@ def _log_cli_enabled(self): return True - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionstart(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("sessionstart") @@ -647,7 +649,7 @@ def pytest_sessionstart(self) -> Generator[None, None, None]: with catching_logs(self.log_file_handler, level=self.log_file_level): yield - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("collection") @@ -655,7 +657,7 @@ def pytest_collection(self) -> Generator[None, None, None]: with catching_logs(self.log_file_handler, level=self.log_file_level): yield - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]: if session.config.option.collectonly: yield @@ -669,12 +671,12 @@ def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]: with catching_logs(self.log_file_handler, level=self.log_file_level): yield # Run all the tests. - @pytest.hookimpl + @hookimpl def pytest_runtest_logstart(self) -> None: self.log_cli_handler.reset() self.log_cli_handler.set_when("start") - @pytest.hookimpl + @hookimpl def pytest_runtest_logreport(self) -> None: self.log_cli_handler.set_when("logreport") @@ -695,7 +697,7 @@ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, Non log = report_handler.stream.getvalue().strip() item.add_report_section(when, "log", log) - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("setup") @@ -703,13 +705,13 @@ def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: item._store[caplog_records_key] = empty yield from self._runtest_for(item, "setup") - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=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") - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("teardown") @@ -717,11 +719,11 @@ def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, Non del item._store[caplog_records_key] del item._store[caplog_handler_key] - @pytest.hookimpl + @hookimpl def pytest_runtest_logfinish(self) -> None: self.log_cli_handler.set_when("finish") - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionfinish(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("sessionfinish") @@ -729,7 +731,7 @@ def pytest_sessionfinish(self) -> Generator[None, None, None]: with catching_logs(self.log_file_handler, level=self.log_file_level): yield - @pytest.hookimpl + @hookimpl def pytest_unconfigure(self) -> None: # Close the FileHandler explicitly. # (logging.shutdown might have lost the weakref?!) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index df4726705d1..a50c7c8d531 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -15,9 +15,9 @@ from typing import TypeVar from typing import Union -import pytest from _pytest.compat import final from _pytest.fixtures import fixture +from _pytest.warning_types import PytestWarning RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") @@ -271,7 +271,7 @@ def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: """ if not isinstance(value, str): warnings.warn( # type: ignore[unreachable] - pytest.PytestWarning( + PytestWarning( "Value of environment variable {name} type should be str, but got " "{value!r} (type: {type}); converted to str implicitly".format( name=name, value=value, type=type(value).__name__ diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 43ccc97c693..0e1dffb9d8a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -34,7 +34,6 @@ from iniconfig import IniConfig from iniconfig import SectionWrapper -import pytest from _pytest import timing from _pytest._code import Source from _pytest.capture import _get_multicapture @@ -42,17 +41,24 @@ from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config import main from _pytest.config import PytestPluginManager from _pytest.config.argparsing import Parser +from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Collector from _pytest.nodes import Item +from _pytest.outcomes import fail +from _pytest.outcomes import importorskip +from _pytest.outcomes import skip from _pytest.pathlib import make_numbered_dir from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.tmpdir import TempPathFactory +from _pytest.warning_types import PytestWarning if TYPE_CHECKING: from typing_extensions import Literal @@ -143,7 +149,7 @@ def matching_platform(self) -> bool: else: return True - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: lines1 = self.get_open_files() yield @@ -165,13 +171,13 @@ def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: "*** function %s:%s: %s " % item.location, "See issue #2366", ] - item.warn(pytest.PytestWarning("\n".join(error))) + item.warn(PytestWarning("\n".join(error))) # used at least by pytest-xdist plugin -@pytest.fixture +@fixture def _pytest(request: FixtureRequest) -> "PytestArg": """Return a helper which offers a gethookrecorder(hook) method which returns a HookRecorder instance which helps to make assertions about called @@ -257,7 +263,7 @@ def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None: break print("NONAMEMATCH", name, "with", call) else: - pytest.fail(f"could not find {name!r} check {check!r}") + fail(f"could not find {name!r} check {check!r}") def popcall(self, name: str) -> ParsedCall: __tracebackhide__ = True @@ -267,7 +273,7 @@ def popcall(self, name: str) -> ParsedCall: return call lines = [f"could not find call {name!r}, in:"] lines.extend([" %s" % x for x in self.calls]) - pytest.fail("\n".join(lines)) + fail("\n".join(lines)) def getcall(self, name: str) -> ParsedCall: values = self.getcalls(name) @@ -417,14 +423,14 @@ def clear(self) -> None: self.calls[:] = [] -@pytest.fixture +@fixture def linecomp() -> "LineComp": """A :class: `LineComp` instance for checking that an input linearly contains a sequence of strings.""" return LineComp() -@pytest.fixture(name="LineMatcher") +@fixture(name="LineMatcher") def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: """A reference to the :class: `LineMatcher`. @@ -434,7 +440,7 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: return LineMatcher -@pytest.fixture +@fixture def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester": """ Facilities to write tests/configuration files, execute pytest in isolation, and match @@ -449,7 +455,7 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt return Pytester(request, tmp_path_factory) -@pytest.fixture +@fixture def testdir(pytester: "Pytester") -> "Testdir": """ Identical to :fixture:`pytester`, and provides an instance whose methods return @@ -460,7 +466,7 @@ def testdir(pytester: "Pytester") -> "Testdir": return Testdir(pytester) -@pytest.fixture +@fixture def _sys_snapshot() -> Generator[None, None, None]: snappaths = SysPathsSnapshot() snapmods = SysModulesSnapshot() @@ -469,7 +475,7 @@ def _sys_snapshot() -> Generator[None, None, None]: snappaths.restore() -@pytest.fixture +@fixture def _config_for_test() -> Generator[Config, None, None]: from _pytest.config import get_config @@ -495,7 +501,7 @@ def __init__( duration: float, ) -> None: try: - self.ret: Union[int, ExitCode] = pytest.ExitCode(ret) + self.ret: Union[int, ExitCode] = ExitCode(ret) """The return value.""" except ValueError: self.ret = ret @@ -1062,7 +1068,7 @@ def pytest_configure(x, config: Config) -> None: rec.append(self.make_hook_recorder(config.pluginmanager)) plugins.append(Collect()) - ret = pytest.main([str(x) for x in args], plugins=plugins) + ret = main([str(x) for x in args], plugins=plugins) if len(rec) == 1: reprec = rec.pop() else: @@ -1448,11 +1454,11 @@ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": The pexpect child is returned. """ - pexpect = pytest.importorskip("pexpect", "3.0") + pexpect = importorskip("pexpect", "3.0") if hasattr(sys, "pypy_version_info") and "64" in platform.machine(): - pytest.skip("pypy-64 bit not supported") + skip("pypy-64 bit not supported") if not hasattr(pexpect, "spawn"): - pytest.skip("pexpect.spawn not available") + skip("pexpect.spawn not available") logfile = self.path.joinpath("spawn.out").open("wb") child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout) @@ -1899,7 +1905,7 @@ def _fail(self, msg: str) -> None: __tracebackhide__ = True log_text = self._log_text self._log_output = [] - pytest.fail(log_text) + fail(log_text) def str(self) -> str: """Return the entire original text.""" diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index ff28be56578..8ea67f3b5bf 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -29,7 +29,7 @@ import pluggy import py -import pytest +import _pytest._version from _pytest import nodes from _pytest import timing from _pytest._code import ExceptionInfo @@ -39,6 +39,7 @@ from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.nodes import Item from _pytest.nodes import Node @@ -259,7 +260,7 @@ def getreportopt(config: Config) -> str: return reportopts -@pytest.hookimpl(trylast=True) # after _pytest.runner +@hookimpl(trylast=True) # after _pytest.runner def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]: letter = "F" if report.passed: @@ -628,7 +629,7 @@ def pytest_collectreport(self, report: CollectReport) -> None: self._add_stats("error", [report]) elif report.skipped: self._add_stats("skipped", [report]) - items = [x for x in report.result if isinstance(x, pytest.Item)] + items = [x for x in report.result if isinstance(x, Item)] self._numcollected += len(items) if self.isatty: self.report_collect() @@ -673,7 +674,7 @@ def report_collect(self, final: bool = False) -> None: else: self.write_line(line) - @pytest.hookimpl(trylast=True) + @hookimpl(trylast=True) def pytest_sessionstart(self, session: "Session") -> None: self._session = session self._sessionstarttime = timing.time() @@ -688,7 +689,7 @@ def pytest_sessionstart(self, session: "Session") -> None: verinfo = ".".join(map(str, pypy_version_info[:3])) msg += "[pypy-{}-{}]".format(verinfo, pypy_version_info[3]) msg += ", pytest-{}, py-{}, pluggy-{}".format( - pytest.__version__, py.__version__, pluggy.__version__ + _pytest._version.version, py.__version__, pluggy.__version__ ) if ( self.verbosity > 0 @@ -783,7 +784,7 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: for line in doc.splitlines(): self._tw.line("{}{}".format(indent + " ", line)) - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_sessionfinish( self, session: "Session", exitstatus: Union[int, ExitCode] ): @@ -810,7 +811,7 @@ def pytest_sessionfinish( self.write_sep("!", str(session.shouldstop), red=True) self.summary_stats() - @pytest.hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_terminal_summary(self) -> Generator[None, None, None]: self.summary_errors() self.summary_failures() diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index b889be88897..4ca1dd6e136 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -8,13 +8,13 @@ import attr import py -import pytest from .pathlib import ensure_reset_dir from .pathlib import LOCK_TIMEOUT from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup from _pytest.compat import final from _pytest.config import Config +from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch @@ -146,14 +146,14 @@ def pytest_configure(config: Config) -> None: mp.setattr(config, "_tmpdirhandler", t, raising=False) -@pytest.fixture(scope="session") +@fixture(scope="session") def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.""" # Set dynamically by pytest_configure() above. return request.config._tmpdirhandler # type: ignore -@pytest.fixture(scope="session") +@fixture(scope="session") def tmp_path_factory(request: FixtureRequest) -> TempPathFactory: """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.""" # Set dynamically by pytest_configure() above. @@ -168,7 +168,7 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: return factory.mktemp(name, numbered=True) -@pytest.fixture +@fixture def tmpdir(tmp_path: Path) -> py.path.local: """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary @@ -181,7 +181,7 @@ def tmpdir(tmp_path: Path) -> py.path.local: return py.path.local(tmp_path) -@pytest.fixture +@fixture def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path: """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary From 361f9e20c318ef3ce6b75fe0dbda0903c3fd9016 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 6 Nov 2020 22:26:25 +0200 Subject: [PATCH 766/823] testing: don't ignore "Module already imported so cannot be rewritten" warning The test suite passes without it being ignored. The absence of this warning cost me some head-scratching time, so enable it again. --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2b25431111e..dd4be6c22d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ filterwarnings = [ # produced by older pyparsing<=2.2.0. "default:Using or importing the ABCs:DeprecationWarning:pyparsing.*", "default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*", - "ignore:Module already imported so cannot be rewritten:pytest.PytestWarning", # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8)." "ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest))", # produced by pytest-xdist From 9bc633064b141894d74220cca537b0646182a176 Mon Sep 17 00:00:00 2001 From: frankgerhardt Date: Sun, 8 Nov 2020 02:44:04 +0100 Subject: [PATCH 767/823] Capitalize headlines (#8008) --- doc/en/example/markers.rst | 2 +- doc/en/example/simple.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 1dc44be3404..f0effa02631 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -544,7 +544,7 @@ Let's run this without capturing output and see what we get: . 1 passed in 0.12s -marking platform specific tests with pytest +Marking platform specific tests with pytest -------------------------------------------------------------- .. regendoc:wipe diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index e9952dad4d7..b641e61f718 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -452,7 +452,7 @@ and nothing when run plainly: ========================== no tests ran in 0.12s =========================== -profiling test duration +Profiling test duration -------------------------- .. regendoc:wipe @@ -498,7 +498,7 @@ Now we can profile which test functions execute the slowest: 0.10s call test_some_are_slow.py::test_funcfast ============================ 3 passed in 0.12s ============================= -incremental testing - test steps +Incremental testing - test steps --------------------------------------------------- .. regendoc:wipe @@ -739,7 +739,7 @@ it (unless you use "autouse" fixture which are always executed ahead of the firs executing). -post-process test reports / failures +Post-process test reports / failures --------------------------------------- If you want to postprocess test reports and need access to the executing From 5b2e5e8a40663e29eb371c6dd8f778dcd8b00861 Mon Sep 17 00:00:00 2001 From: Hugo Martins Date: Sun, 8 Nov 2020 14:45:10 +0000 Subject: [PATCH 768/823] Improve summary stats when using '--collect-only' (#7875) Co-authored-by: Bruno Oliveira --- changelog/7701.improvement.rst | 1 + doc/en/example/nonpython.rst | 2 +- doc/en/example/parametrize.rst | 6 +-- doc/en/example/pythoncollection.rst | 8 +-- doc/en/fixture.rst | 2 +- src/_pytest/terminal.py | 76 ++++++++++++++++++++++++++--- testing/logging/test_reporting.py | 4 +- testing/python/metafunc.py | 2 +- testing/test_cacheprovider.py | 6 +-- testing/test_terminal.py | 42 ++++++++++++++++ 10 files changed, 128 insertions(+), 21 deletions(-) create mode 100644 changelog/7701.improvement.rst diff --git a/changelog/7701.improvement.rst b/changelog/7701.improvement.rst new file mode 100644 index 00000000000..e214be9e3fe --- /dev/null +++ b/changelog/7701.improvement.rst @@ -0,0 +1 @@ +Improved reporting when using ``--collected-only``. It will now show the number of collected tests in the summary stats. diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index 464a6c6cede..558c56772f1 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -102,4 +102,4 @@ interesting to just look at the collection tree: - ========================== no tests ran in 0.12s =========================== + ========================== 2 tests found in 0.12s =========================== diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index f1c98d449fb..d5a11b45192 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -175,7 +175,7 @@ objects, they are still using the default pytest representation: - ========================== no tests ran in 0.12s =========================== + ========================== 8 tests found in 0.12s =========================== In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs together with the actual data, instead of listing them separately. @@ -252,7 +252,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia - ========================== no tests ran in 0.12s =========================== + ========================== 4 tests found in 0.12s =========================== Note that we told ``metafunc.parametrize()`` that your scenario values should be considered class-scoped. With pytest-2.3 this leads to a @@ -328,7 +328,7 @@ Let's first see how it looks like at collection time: - ========================== no tests ran in 0.12s =========================== + ========================== 2/2 tests found in 0.12s =========================== And then when we run the test: diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index c2f0348395c..f7917b790ef 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -157,7 +157,7 @@ The test collection would look like this: - ========================== no tests ran in 0.12s =========================== + ========================== 2 tests found in 0.12s =========================== You can check for multiple glob patterns by adding a space between the patterns: @@ -220,7 +220,7 @@ You can always peek at the collection tree without running tests like this: - ========================== no tests ran in 0.12s =========================== + ========================== 3 tests found in 0.12s =========================== .. _customizing-test-collection: @@ -282,7 +282,7 @@ leave out the ``setup.py`` file: - ====== no tests ran in 0.04 seconds ====== + ====== 1 tests found in 0.04 seconds ====== If you run with a Python 3 interpreter both the one test and the ``setup.py`` file will be left out: @@ -296,7 +296,7 @@ file will be left out: rootdir: $REGENDOC_TMPDIR, configfile: pytest.ini collected 0 items - ========================== no tests ran in 0.12s =========================== + ========================== no tests found in 0.12s =========================== It's also possible to ignore files based on Unix shell-style wildcards by adding patterns to :globalvar:`collect_ignore_glob`. diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 90e88d87620..a0411902c0b 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -919,7 +919,7 @@ Running the above tests results in the following test IDs being used: - ========================== no tests ran in 0.12s =========================== + ========================== 10 tests found in 0.12s =========================== .. _`fixture-parametrize-marks`: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 8ea67f3b5bf..f1736ee4309 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1163,15 +1163,45 @@ def _set_main_color(self) -> None: self._main_color = self._determine_main_color(bool(unknown_types)) def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]: - main_color, known_types = self._get_main_color() + """ + Build the parts used in the last summary stats line. + + The summary stats line is the line shown at the end, "=== 12 passed, 2 errors in Xs===". + + This function builds a list of the "parts" that make up for the text in that line, in + the example above it would be: + [ + ("12 passed", {"green": True}), + ("2 errors", {"red": True} + ] + + That last dict for each line is a "markup dictionary", used by TerminalWriter to + color output. + + The final color of the line is also determined by this function, and is the second + element of the returned tuple. + """ + if self.config.getoption("collectonly"): + return self._build_collect_only_summary_stats_line() + else: + return self._build_normal_summary_stats_line() + + def _get_reports_to_display(self, key: str) -> List[Any]: + """Get test/collection reports for the given status key, such as `passed` or `error`.""" + reports = self.stats.get(key, []) + return [x for x in reports if getattr(x, "count_towards_summary", True)] + + def _build_normal_summary_stats_line( + self, + ) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]: + main_color, known_types = self._get_main_color() parts = [] + for key in known_types: - reports = self.stats.get(key, None) + reports = self._get_reports_to_display(key) if reports: - count = sum( - 1 for rep in reports if getattr(rep, "count_towards_summary", True) - ) + count = len(reports) color = _color_for_type.get(key, _color_for_type_default) markup = {color: True, "bold": color == main_color} parts.append(("%d %s" % _make_plural(count, key), markup)) @@ -1181,6 +1211,40 @@ def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], s return parts, main_color + def _build_collect_only_summary_stats_line( + self, + ) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]: + deselected = len(self._get_reports_to_display("deselected")) + errors = len(self._get_reports_to_display("error")) + + if self._numcollected == 0: + parts = [("no tests collected", {"yellow": True})] + main_color = "yellow" + + elif deselected == 0: + main_color = "green" + collected_output = "%d %s collected" % _make_plural( + self._numcollected, "test" + ) + parts = [(collected_output, {main_color: True})] + else: + all_tests_were_deselected = self._numcollected == deselected + if all_tests_were_deselected: + main_color = "yellow" + collected_output = f"no tests collected ({deselected} deselected)" + else: + main_color = "green" + selected = self._numcollected - deselected + collected_output = f"{selected}/{self._numcollected} tests collected ({deselected} deselected)" + + parts = [(collected_output, {main_color: True})] + + if errors: + main_color = _color_for_type["error"] + parts += [("%d %s" % _make_plural(errors, "error"), {main_color: True})] + + return parts, main_color + def _get_pos(config: Config, rep: BaseReport): nodeid = config.cwd_relative_nodeid(rep.nodeid) @@ -1267,7 +1331,7 @@ def _folded_skips( def _make_plural(count: int, noun: str) -> Tuple[int, str]: # No need to pluralize words such as `failed` or `passed`. - if noun not in ["error", "warnings"]: + if noun not in ["error", "warnings", "test"]: return count, noun # The `warnings` key is plural. To avoid API breakage, we keep it that way but diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 7545d016d52..fc9f1082346 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -889,7 +889,7 @@ def test_simple(): [ "*collected 1 item*", "**", - "*no tests ran*", + "*1 test collected*", ] ) elif verbose == "-q": @@ -897,7 +897,7 @@ def test_simple(): expected_lines.extend( [ "*test_collection_collect_only_live_logging.py::test_simple*", - "no tests ran in [0-9].[0-9][0-9]s", + "1 test collected in [0-9].[0-9][0-9]s", ] ) elif verbose == "-qq": diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 2a6b3dc5414..676f1d988bc 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1417,7 +1417,7 @@ def test_foo(x): ' @pytest.mark.parametrise("x", range(2))', "E Failed: Unknown 'parametrise' mark, did you mean 'parametrize'?", "*! Interrupted: 1 error during collection !*", - "*= 1 error in *", + "*= no tests collected, 1 error in *", ] ) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 54e657b27f5..37253b8b5db 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -909,7 +909,7 @@ def test_fail(): assert 0 "", "", " ", - "*= 1 deselected in *", + "*= 1/2 tests collected (1 deselected) in *", ], ) @@ -942,7 +942,7 @@ def test_other(): assert 0 " ", " ", "", - "*= 1 deselected in *", + "*= 2/3 tests collected (1 deselected) in *", ], consecutive=True, ) @@ -977,7 +977,7 @@ def test_pass(): pass "", " ", "", - "*= no tests ran in*", + "*= 1 test collected in*", ], consecutive=True, ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 77bd2ace64d..0b861f25a7b 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -458,6 +458,48 @@ def test_collectonly_more_quiet(self, testdir): result = testdir.runpytest("--collect-only", "-qq") result.stdout.fnmatch_lines(["*test_fun.py: 1*"]) + def test_collect_only_summary_status(self, testdir): + """Custom status depending on test selection using -k or -m. #7701.""" + testdir.makepyfile( + test_collect_foo=""" + def test_foo(): pass + """, + test_collect_bar=""" + def test_foobar(): pass + def test_bar(): pass + """, + ) + result = testdir.runpytest("--collect-only") + result.stdout.fnmatch_lines("*== 3 tests collected in * ==*") + + result = testdir.runpytest("--collect-only", "test_collect_foo.py") + result.stdout.fnmatch_lines("*== 1 test collected in * ==*") + + result = testdir.runpytest("--collect-only", "-k", "foo") + result.stdout.fnmatch_lines("*== 2/3 tests collected (1 deselected) in * ==*") + + result = testdir.runpytest("--collect-only", "-k", "test_bar") + result.stdout.fnmatch_lines("*== 1/3 tests collected (2 deselected) in * ==*") + + result = testdir.runpytest("--collect-only", "-k", "invalid") + result.stdout.fnmatch_lines("*== no tests collected (3 deselected) in * ==*") + + testdir.mkdir("no_tests_here") + result = testdir.runpytest("--collect-only", "no_tests_here") + result.stdout.fnmatch_lines("*== no tests collected in * ==*") + + testdir.makepyfile( + test_contains_error=""" + raise RuntimeError + """, + ) + result = testdir.runpytest("--collect-only") + result.stdout.fnmatch_lines("*== 3 tests collected, 1 error in * ==*") + result = testdir.runpytest("--collect-only", "-k", "foo") + result.stdout.fnmatch_lines( + "*== 2/3 tests collected (1 deselected), 1 error in * ==*" + ) + class TestFixtureReporting: def test_setup_fixture_error(self, testdir): From c7f8ad17f50fc77db0c646a55a3950c3306156f6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 8 Nov 2020 12:42:52 -0300 Subject: [PATCH 769/823] Rename _make_plural to pluralize A bit shorter and a better name, IMHO. --- src/_pytest/terminal.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index f1736ee4309..7d2943dd01e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -1204,7 +1204,7 @@ def _build_normal_summary_stats_line( count = len(reports) color = _color_for_type.get(key, _color_for_type_default) markup = {color: True, "bold": color == main_color} - parts.append(("%d %s" % _make_plural(count, key), markup)) + parts.append(("%d %s" % pluralize(count, key), markup)) if not parts: parts = [("no tests ran", {_color_for_type_default: True})] @@ -1223,9 +1223,7 @@ def _build_collect_only_summary_stats_line( elif deselected == 0: main_color = "green" - collected_output = "%d %s collected" % _make_plural( - self._numcollected, "test" - ) + collected_output = "%d %s collected" % pluralize(self._numcollected, "test") parts = [(collected_output, {main_color: True})] else: all_tests_were_deselected = self._numcollected == deselected @@ -1241,7 +1239,7 @@ def _build_collect_only_summary_stats_line( if errors: main_color = _color_for_type["error"] - parts += [("%d %s" % _make_plural(errors, "error"), {main_color: True})] + parts += [("%d %s" % pluralize(errors, "error"), {main_color: True})] return parts, main_color @@ -1329,7 +1327,7 @@ def _folded_skips( _color_for_type_default = "yellow" -def _make_plural(count: int, noun: str) -> Tuple[int, str]: +def pluralize(count: int, noun: str) -> Tuple[int, str]: # No need to pluralize words such as `failed` or `passed`. if noun not in ["error", "warnings", "test"]: return count, noun From 02d4b3d75fd961046824a25e555283a3861ba026 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 03:17:35 +0000 Subject: [PATCH 770/823] build(deps): bump django in /testing/plugins_integration Bumps [django](https://github.com/django/django) from 3.1.2 to 3.1.3. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.1.2...3.1.3) 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 cdd55a0e51d..78eba3155a6 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,5 +1,5 @@ anyio[curio,trio]==2.0.2 -django==3.1.2 +django==3.1.3 pytest-asyncio==0.14.0 pytest-bdd==4.0.1 pytest-cov==2.10.1 From 6f13d1b03b1e1af7def99505234075878407767d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 6 Nov 2020 19:27:33 +0200 Subject: [PATCH 771/823] Export MonkeyPatch as pytest.MonkeyPatch We want to export `pytest.MonkeyPatch` for the purpose of type-annotating the `monkeypatch` fixture. For other fixtures we export in this way, we also make direct construction of them (e.g. `MonkeyPatch()`) private. But unlike the others, `MonkeyPatch` is also widely used directly already, mostly because the `monkeypatch` fixture only works in `function` scope (issue #363), but also in other cases. So making it private will be annoying and we don't offer a decent replacement yet. So, let's just make direct construction public & documented. --- changelog/8006.feature.rst | 8 ++++++++ doc/en/reference.rst | 6 ++---- src/_pytest/monkeypatch.py | 18 ++++++++++++++---- src/pytest/__init__.py | 2 ++ testing/test_monkeypatch.py | 10 ++++++++++ 5 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 changelog/8006.feature.rst diff --git a/changelog/8006.feature.rst b/changelog/8006.feature.rst new file mode 100644 index 00000000000..0203689ba4b --- /dev/null +++ b/changelog/8006.feature.rst @@ -0,0 +1,8 @@ +It is now possible to construct a :class:`~pytest.MonkeyPatch` object directly as ``pytest.MonkeyPatch()``, +in cases when the :fixture:`monkeypatch` fixture cannot be used. Previously some users imported it +from the private `_pytest.monkeypatch.MonkeyPatch` namespace. + +Additionally, :meth:`MonkeyPatch.context ` is now a classmethod, +and can be used as ``with MonkeyPatch.context() as mp: ...``. This is the recommended way to use +``MonkeyPatch`` directly, since unlike the ``monkeypatch`` fixture, an instance created directly +is not ``undo()``-ed automatically. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index c04b8da0b1b..cbe89fe0bf0 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -486,16 +486,14 @@ caplog monkeypatch ~~~~~~~~~~~ -.. currentmodule:: _pytest.monkeypatch - **Tutorial**: :doc:`monkeypatch`. .. autofunction:: _pytest.monkeypatch.monkeypatch() :no-auto-options: - Returns a :class:`MonkeyPatch` instance. + Returns a :class:`~pytest.MonkeyPatch` instance. -.. autoclass:: _pytest.monkeypatch.MonkeyPatch +.. autoclass:: pytest.MonkeyPatch :members: diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index df4726705d1..31b7b125b3c 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -111,8 +111,17 @@ def __repr__(self) -> str: @final class MonkeyPatch: - """Object returned by the ``monkeypatch`` fixture keeping a record of - setattr/item/env/syspath changes.""" + """Helper to conveniently monkeypatch attributes/items/environment + variables/syspath. + + Returned by the :fixture:`monkeypatch` fixture. + + :versionchanged:: 6.2 + Can now also be used directly as `pytest.MonkeyPatch()`, for when + the fixture is not available. In this case, use + :meth:`with MonkeyPatch.context() as mp: ` or remember to call + :meth:`undo` explicitly. + """ def __init__(self) -> None: self._setattr: List[Tuple[object, str, object]] = [] @@ -120,8 +129,9 @@ def __init__(self) -> None: self._cwd: Optional[str] = None self._savesyspath: Optional[List[str]] = None + @classmethod @contextmanager - def context(self) -> Generator["MonkeyPatch", None, None]: + def context(cls) -> Generator["MonkeyPatch", None, None]: """Context manager that returns a new :class:`MonkeyPatch` object which undoes any patching done inside the ``with`` block upon exit. @@ -140,7 +150,7 @@ def test_partial(monkeypatch): such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples of this see `#3290 `_. """ - m = MonkeyPatch() + m = cls() try: yield m finally: diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index a9c1ee0282b..d7a5b22997f 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -19,6 +19,7 @@ from _pytest.main import Session from _pytest.mark import MARK_GEN as mark from _pytest.mark import param +from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Collector from _pytest.nodes import File from _pytest.nodes import Item @@ -74,6 +75,7 @@ "main", "mark", "Module", + "MonkeyPatch", "Package", "param", "PytestAssertRewriteWarning", diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 73fe313e5c9..c20ff7480a8 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -409,6 +409,16 @@ def test_context() -> None: assert inspect.isclass(functools.partial) +def test_context_classmethod() -> None: + class A: + x = 1 + + with MonkeyPatch.context() as m: + m.setattr(A, "x", 2) + assert A.x == 2 + assert A.x == 1 + + def test_syspath_prepend_with_namespace_packages( testdir: Testdir, monkeypatch: MonkeyPatch ) -> None: From 043ed55056ff858fc2f28f0f9239843b7085b66d Mon Sep 17 00:00:00 2001 From: Josias Aurel Date: Mon, 9 Nov 2020 18:07:34 +0100 Subject: [PATCH 772/823] Migrate from testdir to pytester --- testing/test_faulthandler.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index 4d0b32eded7..fea2681ba0f 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -1,26 +1,26 @@ import sys import pytest +from _pytest.pytester import Pytester - -def test_enabled(testdir): +def test_enabled(pytester: Pytester) -> None: """Test single crashing test displays a traceback.""" - testdir.makepyfile( + pytester.makepyfile( """ import faulthandler def test_crash(): faulthandler._sigabrt() """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stderr.fnmatch_lines(["*Fatal Python error*"]) assert result.ret != 0 -def test_crash_near_exit(testdir): +def test_crash_near_exit(pytester: Pytester) -> None: """Test that fault handler displays crashes that happen even after pytest is exiting (for example, when the interpreter is shutting down).""" - testdir.makepyfile( + pytester.makepyfile( """ import faulthandler import atexit @@ -28,21 +28,21 @@ def test_ok(): atexit.register(faulthandler._sigabrt) """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stderr.fnmatch_lines(["*Fatal Python error*"]) assert result.ret != 0 -def test_disabled(testdir): +def test_disabled(pytester: Pytester) -> None: """Test option to disable fault handler in the command line.""" - testdir.makepyfile( + pytester.makepyfile( """ import faulthandler def test_disabled(): assert not faulthandler.is_enabled() """ ) - result = testdir.runpytest_subprocess("-p", "no:faulthandler") + result = pytester.runpytest_subprocess("-p", "no:faulthandler") result.stdout.fnmatch_lines(["*1 passed*"]) assert result.ret == 0 @@ -56,19 +56,19 @@ def test_disabled(): False, ], ) -def test_timeout(testdir, enabled: bool) -> None: +def test_timeout(pytester: Pytester, enabled: bool) -> None: """Test option to dump tracebacks after a certain timeout. If faulthandler is disabled, no traceback will be dumped. """ - testdir.makepyfile( + pytester.makepyfile( """ import os, time def test_timeout(): time.sleep(1 if "CI" in os.environ else 0.1) """ ) - testdir.makeini( + pytester.makeini( """ [pytest] faulthandler_timeout = 0.01 @@ -76,7 +76,7 @@ def test_timeout(): ) args = ["-p", "no:faulthandler"] if not enabled else [] - result = testdir.runpytest_subprocess(*args) + result = pytester.runpytest_subprocess(*args) tb_output = "most recent call first" if enabled: result.stderr.fnmatch_lines(["*%s*" % tb_output]) @@ -108,21 +108,21 @@ def test_cancel_timeout_on_hook(monkeypatch, hook_name): @pytest.mark.parametrize("faulthandler_timeout", [0, 2]) -def test_already_initialized(faulthandler_timeout, testdir): +def test_already_initialized(faulthandler_timeout, pytester: Pytester): """Test for faulthandler being initialized earlier than pytest (#6575).""" - testdir.makepyfile( + pytester.makepyfile( """ def test(): import faulthandler assert faulthandler.is_enabled() """ ) - result = testdir.run( + result = pytester.run( sys.executable, "-X", "faulthandler", "-mpytest", - testdir.tmpdir, + pytester.tmpdir, "-o", f"faulthandler_timeout={faulthandler_timeout}", ) From 265cc2cfece4ee5a2094b7c02dd80cd6099af60f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 9 Nov 2020 14:08:27 +0000 Subject: [PATCH 773/823] main: fix only one doctest collected on pytest --doctest-modules __init__.py When --doctest-modules is used, an `__init__.py` file is not a `Package` but a `DoctestModule`, but some collection code assumed that `__init__.py` implies a `Package`. That code caused only a single test to be collected in the scenario in the subject. Tighten up this check to explicitly check for `Package`. There are better solutions, but for another time. Report & test by Nick Gates . --- changelog/8016.bugfix.rst | 1 + src/_pytest/main.py | 14 ++++++++------ testing/test_doctest.py | 11 ++++++++--- 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 changelog/8016.bugfix.rst diff --git a/changelog/8016.bugfix.rst b/changelog/8016.bugfix.rst new file mode 100644 index 00000000000..94539af5c97 --- /dev/null +++ b/changelog/8016.bugfix.rst @@ -0,0 +1 @@ +Fixed only one doctest being collected when using ``pytest --doctest-modules path/to/an/__init__.py``. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 04b51ac00fb..4234ae9517e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -765,12 +765,14 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: self._notfound.append((report_arg, col)) continue - # If __init__.py was the only file requested, then the matched node will be - # the corresponding Package, and the first yielded item will be the __init__ - # Module itself, so just use that. If this special case isn't taken, then all - # the files in the package will be yielded. - if argpath.basename == "__init__.py": - assert isinstance(matching[0], nodes.Collector) + # If __init__.py was the only file requested, then the matched + # node will be the corresponding Package (by default), and the + # first yielded item will be the __init__ Module itself, so + # just use that. If this special case isn't taken, then all the + # files in the package will be yielded. + if argpath.basename == "__init__.py" and isinstance( + matching[0], Package + ): try: yield next(iter(matching[0].collect())) except StopIteration: diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 8f31cb60643..6e3880330a9 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -68,9 +68,13 @@ def my_func(): assert isinstance(items[0].parent, DoctestModule) assert items[0].parent is items[1].parent - def test_collect_module_two_doctest_no_modulelevel(self, pytester: Pytester): + @pytest.mark.parametrize("filename", ["__init__", "whatever"]) + def test_collect_module_two_doctest_no_modulelevel( + self, pytester: Pytester, filename: str, + ) -> None: path = pytester.makepyfile( - whatever=""" + **{ + filename: """ '# Empty' def my_func(): ">>> magic = 42 " @@ -84,7 +88,8 @@ def another(): # This is another function >>> import os # this one does have a doctest ''' - """ + """, + }, ) for p in (path, pytester.path): items, reprec = pytester.inline_genitems(p, "--doctest-modules") From 39b2706f6a8f5129a9d5ea69fe302c86d574fbf0 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 11 Nov 2020 01:41:25 +0000 Subject: [PATCH 774/823] Add 'node_modules' to norecursedirs Fixes #8023. --- changelog/8023.improvement.rst | 1 + doc/en/reference.rst | 3 ++- src/_pytest/main.py | 12 +++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 changelog/8023.improvement.rst diff --git a/changelog/8023.improvement.rst b/changelog/8023.improvement.rst new file mode 100644 index 00000000000..8d005ba0c70 --- /dev/null +++ b/changelog/8023.improvement.rst @@ -0,0 +1 @@ +Added ``'node_modules'`` to default value for ``norecursedirs``. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index cbe89fe0bf0..34be2c4540a 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1541,7 +1541,8 @@ passed multiple times. The expected format is ``name=value``. For example:: [seq] matches any character in seq [!seq] matches any char not in seq - Default patterns are ``'.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg', 'venv'``. + Default patterns are ``'*.egg'``, ``'.*'``, ``'_darcs'``, ``'build'``, + ``'CVS'``, ``'dist'``, ``'node_modules'``, ``'venv'``, ``'{arch}'``. Setting a ``norecursedirs`` replaces the default. Here is an example of how to avoid certain directories: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 04b51ac00fb..93073226c56 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -53,7 +53,17 @@ def pytest_addoption(parser: Parser) -> None: "norecursedirs", "directory patterns to avoid for recursion", type="args", - default=[".*", "build", "dist", "CVS", "_darcs", "{arch}", "*.egg", "venv"], + default=[ + "*.egg", + ".*", + "_darcs", + "build", + "CVS", + "dist", + "node_modules", + "venv", + "{arch}", + ], ) parser.addini( "testpaths", From 8320c071348e6d27ad789a7cbe017e01856dd835 Mon Sep 17 00:00:00 2001 From: Josias Aurel Date: Wed, 11 Nov 2020 04:45:42 +0100 Subject: [PATCH 775/823] Update testing/test_faulthandler.py Co-authored-by: Sanket Duthade --- testing/test_faulthandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index fea2681ba0f..38a3cddceee 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -108,7 +108,7 @@ def test_cancel_timeout_on_hook(monkeypatch, hook_name): @pytest.mark.parametrize("faulthandler_timeout", [0, 2]) -def test_already_initialized(faulthandler_timeout, pytester: Pytester): +def test_already_initialized(faulthandler_timeout: int, pytester: Pytester) -> None: """Test for faulthandler being initialized earlier than pytest (#6575).""" pytester.makepyfile( """ From 1ed8159c7d87ee3f11d4ed428bd51f3a06d7e5d9 Mon Sep 17 00:00:00 2001 From: Josias Aurel Date: Wed, 11 Nov 2020 04:45:57 +0100 Subject: [PATCH 776/823] Update testing/test_faulthandler.py Co-authored-by: Sanket Duthade --- testing/test_faulthandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index 38a3cddceee..7015238d82c 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -122,7 +122,7 @@ def test(): "-X", "faulthandler", "-mpytest", - pytester.tmpdir, + pytester.path, "-o", f"faulthandler_timeout={faulthandler_timeout}", ) From 06a597db14af95dbe22b87bfe1391bc22fd1857b Mon Sep 17 00:00:00 2001 From: Josias Aurel Date: Wed, 11 Nov 2020 05:02:32 +0100 Subject: [PATCH 777/823] Add type annotations --- testing/test_faulthandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index 7015238d82c..e25424fc036 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -87,7 +87,7 @@ def test_timeout(): @pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"]) -def test_cancel_timeout_on_hook(monkeypatch, hook_name): +def test_cancel_timeout_on_hook(monkeypatch, hook_name) -> None: """Make sure that we are cancelling any scheduled traceback dumping due to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any other interactive exception (pytest-dev/pytest-faulthandler#14).""" From fa148eadfe9ed1547f5a6122d9f51d0302f42c43 Mon Sep 17 00:00:00 2001 From: Josias Aurel Date: Wed, 11 Nov 2020 18:35:06 +0100 Subject: [PATCH 778/823] Update testing/test_faulthandler.py Co-authored-by: Sanket Duthade --- testing/test_faulthandler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index e25424fc036..caf39813cf4 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -3,6 +3,7 @@ import pytest from _pytest.pytester import Pytester + def test_enabled(pytester: Pytester) -> None: """Test single crashing test displays a traceback.""" pytester.makepyfile( From b0505788821604f0b0787683d47a0ca693fd0426 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 9 Nov 2020 18:27:40 +0200 Subject: [PATCH 779/823] pytester: split asserts to a separate plugin, don't rewrite pytester itself An upcoming commit wants to import from `_pytest.pytester` in the public `pytest` module. This means that `_pytest.pytester` would start to get imported during import time, which it hasn't up to now -- it was imported by the plugin loader (if requested). When a plugin is loaded, it is subjected to assertion rewriting, but only if the module isn't imported yet, it issues a warning "Module already imported so cannot be rewritten" and skips the rewriting. So we'd end up with the pytester plugin not being rewritten, but it wants to be. Absent better ideas, the solution here is to split the pytester assertions to their own plugin (which will always only be imported by the plugin loader) and exclude pytester itself from plugin rewriting. --- src/_pytest/config/__init__.py | 1 + src/_pytest/pytester.py | 51 +++++++++++------------ src/_pytest/pytester_assertions.py | 66 ++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 28 deletions(-) create mode 100644 src/_pytest/pytester_assertions.py diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6c1d9c69a50..74168efd572 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -256,6 +256,7 @@ def directory_arg(path: str, optname: str) -> str: builtin_plugins = set(default_plugins) builtin_plugins.add("pytester") +builtin_plugins.add("pytester_assertions") def get_config( diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 0e1dffb9d8a..158b71b3a52 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1,4 +1,7 @@ -"""(Disabled by default) support for testing pytest and pytest plugins.""" +"""(Disabled by default) support for testing pytest and pytest plugins. + +PYTEST_DONT_REWRITE +""" import collections.abc import contextlib import gc @@ -66,6 +69,9 @@ import pexpect +pytest_plugins = ["pytester_assertions"] + + IGNORE_PAM = [ # filenames added when obtaining details about the current user "/var/lib/sss/mc/passwd" ] @@ -408,16 +414,12 @@ def countoutcomes(self) -> List[int]: def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: __tracebackhide__ = True + from _pytest.pytester_assertions import assertoutcome outcomes = self.listoutcomes() - realpassed, realskipped, realfailed = outcomes - obtained = { - "passed": len(realpassed), - "skipped": len(realskipped), - "failed": len(realfailed), - } - expected = {"passed": passed, "skipped": skipped, "failed": failed} - assert obtained == expected, outcomes + assertoutcome( + outcomes, passed=passed, skipped=skipped, failed=failed, + ) def clear(self) -> None: self.calls[:] = [] @@ -574,25 +576,18 @@ def assert_outcomes( """Assert that the specified outcomes appear with the respective numbers (0 means it didn't occur) in the text output from a test run.""" __tracebackhide__ = True - - d = self.parseoutcomes() - obtained = { - "passed": d.get("passed", 0), - "skipped": d.get("skipped", 0), - "failed": d.get("failed", 0), - "errors": d.get("errors", 0), - "xpassed": d.get("xpassed", 0), - "xfailed": d.get("xfailed", 0), - } - expected = { - "passed": passed, - "skipped": skipped, - "failed": failed, - "errors": errors, - "xpassed": xpassed, - "xfailed": xfailed, - } - assert obtained == expected + from _pytest.pytester_assertions import assert_outcomes + + outcomes = self.parseoutcomes() + assert_outcomes( + outcomes, + passed=passed, + skipped=skipped, + failed=failed, + errors=errors, + xpassed=xpassed, + xfailed=xfailed, + ) class CwdSnapshot: diff --git a/src/_pytest/pytester_assertions.py b/src/_pytest/pytester_assertions.py new file mode 100644 index 00000000000..630c1d3331c --- /dev/null +++ b/src/_pytest/pytester_assertions.py @@ -0,0 +1,66 @@ +"""Helper plugin for pytester; should not be loaded on its own.""" +# This plugin contains assertions used by pytester. pytester cannot +# contain them itself, since it is imported by the `pytest` module, +# hence cannot be subject to assertion rewriting, which requires a +# module to not be already imported. +from typing import Dict +from typing import Sequence +from typing import Tuple +from typing import Union + +from _pytest.reports import CollectReport +from _pytest.reports import TestReport + + +def assertoutcome( + outcomes: Tuple[ + Sequence[TestReport], + Sequence[Union[CollectReport, TestReport]], + Sequence[Union[CollectReport, TestReport]], + ], + passed: int = 0, + skipped: int = 0, + failed: int = 0, +) -> None: + __tracebackhide__ = True + + realpassed, realskipped, realfailed = outcomes + obtained = { + "passed": len(realpassed), + "skipped": len(realskipped), + "failed": len(realfailed), + } + expected = {"passed": passed, "skipped": skipped, "failed": failed} + assert obtained == expected, outcomes + + +def assert_outcomes( + outcomes: Dict[str, int], + passed: int = 0, + skipped: int = 0, + failed: int = 0, + errors: int = 0, + xpassed: int = 0, + xfailed: int = 0, +) -> None: + """Assert that the specified outcomes appear with the respective + numbers (0 means it didn't occur) in the text output from a test run.""" + __tracebackhide__ = True + + obtained = { + "passed": outcomes.get("passed", 0), + "skipped": outcomes.get("skipped", 0), + "failed": outcomes.get("failed", 0), + "errors": outcomes.get("errors", 0), + "xpassed": outcomes.get("xpassed", 0), + "xfailed": outcomes.get("xfailed", 0), + } + expected = { + "passed": passed, + "skipped": skipped, + "failed": failed, + "errors": errors, + "xpassed": xpassed, + "xfailed": xfailed, + } + assert obtained == expected From f1e6fdcddbfe8991935685ccc5049dd957ec4382 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 27 Sep 2020 22:20:31 +0300 Subject: [PATCH 780/823] Export types of builtin fixture for type annotations In order to allow users to type annotate fixtures they request, the types need to be imported from the `pytest` namespace. They are/were always available to import from the `_pytest` namespace, but that is not guaranteed to be stable. These types are only exported for the purpose of typing. Specifically, the following are *not* public: - Construction (`__init__`) - Subclassing - staticmethods and classmethods We try to combat them being used anyway by: - Marking the classes as `@final` when possible (already done). - Not documenting private stuff in the API Reference. - Using `_`-prefixed names or marking as `:meta private:` for private stuff. - Adding a keyword-only `_ispytest=False` to private constructors, warning if False, and changing pytest itself to pass True. In the future it will (hopefully) become a hard error. Hopefully that will be enough. --- changelog/7469.deprecation.rst | 18 ++++++++ changelog/7469.improvement.rst | 23 ++++++++++ doc/en/reference.rst | 77 +++++++++++++--------------------- src/_pytest/cacheprovider.py | 51 ++++++++++++++++------ src/_pytest/capture.py | 18 ++++---- src/_pytest/deprecated.py | 26 ++++++++++++ src/_pytest/doctest.py | 2 +- src/_pytest/fixtures.py | 15 +++++-- src/_pytest/logging.py | 6 ++- src/_pytest/pytester.py | 18 +++++--- src/_pytest/python.py | 2 +- src/_pytest/recwarn.py | 15 ++++--- src/_pytest/tmpdir.py | 61 +++++++++++++++++++-------- src/pytest/__init__.py | 18 ++++++++ testing/deprecated_test.py | 14 +++++++ testing/python/fixtures.py | 22 +++++----- testing/test_cacheprovider.py | 6 +-- testing/test_recwarn.py | 16 +++---- testing/test_tmpdir.py | 6 ++- 19 files changed, 290 insertions(+), 124 deletions(-) create mode 100644 changelog/7469.deprecation.rst create mode 100644 changelog/7469.improvement.rst diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst new file mode 100644 index 00000000000..67d0b2bba46 --- /dev/null +++ b/changelog/7469.deprecation.rst @@ -0,0 +1,18 @@ +Directly constructing/calling the following classes/functions is now deprecated: + +- ``_pytest.cacheprovider.Cache`` +- ``_pytest.cacheprovider.Cache.for_config()`` +- ``_pytest.cacheprovider.Cache.clear_cache()`` +- ``_pytest.cacheprovider.Cache.cache_dir_from_config()`` +- ``_pytest.capture.CaptureFixture`` +- ``_pytest.fixtures.FixtureRequest`` +- ``_pytest.fixtures.SubRequest`` +- ``_pytest.logging.LogCaptureFixture`` +- ``_pytest.pytester.Pytester`` +- ``_pytest.pytester.Testdir`` +- ``_pytest.recwarn.WarningsRecorder`` +- ``_pytest.recwarn.WarningsChecker`` +- ``_pytest.tmpdir.TempPathFactory`` +- ``_pytest.tmpdir.TempdirFactory`` + +These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0. diff --git a/changelog/7469.improvement.rst b/changelog/7469.improvement.rst new file mode 100644 index 00000000000..cbd75f05419 --- /dev/null +++ b/changelog/7469.improvement.rst @@ -0,0 +1,23 @@ +It is now possible to construct a :class:`MonkeyPatch` object directly as ``pytest.MonkeyPatch()``, +in cases when the :fixture:`monkeypatch` fixture cannot be used. Previously some users imported it +from the private `_pytest.monkeypatch.MonkeyPatch` namespace. + +The types of builtin pytest fixtures are now exported so they may be used in type annotations of test functions. +The newly-exported types are: + +- ``pytest.FixtureRequest`` for the :fixture:`request` fixture. +- ``pytest.Cache`` for the :fixture:`cache` fixture. +- ``pytest.CaptureFixture[str]`` for the :fixture:`capfd` and :fixture:`capsys` fixtures. +- ``pytest.CaptureFixture[bytes]`` for the :fixture:`capfdbinary` and :fixture:`capsysbinary` fixtures. +- ``pytest.LogCaptureFixture`` for the :fixture:`caplog` fixture. +- ``pytest.Pytester`` for the :fixture:`pytester` fixture. +- ``pytest.Testdir`` for the :fixture:`testdir` fixture. +- ``pytest.TempdirFactory`` for the :fixture:`tmpdir_factory` fixture. +- ``pytest.TempPathFactory`` for the :fixture:`tmp_path_factory` fixture. +- ``pytest.MonkeyPatch`` for the :fixture:`monkeypatch` fixture. +- ``pytest.WarningsRecorder`` for the :fixture:`recwarn` fixture. + +Constructing them is not supported (except for `MonkeyPatch`); they are only meant for use in type annotations. +Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0. + +Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index cbe89fe0bf0..6973043ccfe 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -304,11 +304,10 @@ request ``pytestconfig`` into your fixture and get it with ``pytestconfig.cache` Under the hood, the cache plugin uses the simple ``dumps``/``loads`` API of the :py:mod:`json` stdlib module. -.. currentmodule:: _pytest.cacheprovider +``config.cache`` is an instance of :class:`pytest.Cache`: -.. automethod:: Cache.get -.. automethod:: Cache.set -.. automethod:: Cache.makedir +.. autoclass:: pytest.Cache() + :members: .. fixture:: capsys @@ -318,12 +317,10 @@ capsys **Tutorial**: :doc:`capture`. -.. currentmodule:: _pytest.capture - -.. autofunction:: capsys() +.. autofunction:: _pytest.capture.capsys() :no-auto-options: - Returns an instance of :py:class:`CaptureFixture`. + Returns an instance of :class:`CaptureFixture[str] `. Example: @@ -334,7 +331,7 @@ capsys captured = capsys.readouterr() assert captured.out == "hello\n" -.. autoclass:: CaptureFixture() +.. autoclass:: pytest.CaptureFixture() :members: @@ -345,10 +342,10 @@ capsysbinary **Tutorial**: :doc:`capture`. -.. autofunction:: capsysbinary() +.. autofunction:: _pytest.capture.capsysbinary() :no-auto-options: - Returns an instance of :py:class:`CaptureFixture`. + Returns an instance of :class:`CaptureFixture[bytes] `. Example: @@ -367,10 +364,10 @@ capfd **Tutorial**: :doc:`capture`. -.. autofunction:: capfd() +.. autofunction:: _pytest.capture.capfd() :no-auto-options: - Returns an instance of :py:class:`CaptureFixture`. + Returns an instance of :class:`CaptureFixture[str] `. Example: @@ -389,10 +386,10 @@ capfdbinary **Tutorial**: :doc:`capture`. -.. autofunction:: capfdbinary() +.. autofunction:: _pytest.capture.capfdbinary() :no-auto-options: - Returns an instance of :py:class:`CaptureFixture`. + Returns an instance of :class:`CaptureFixture[bytes] `. Example: @@ -433,7 +430,7 @@ request The ``request`` fixture is a special fixture providing information of the requesting test function. -.. autoclass:: _pytest.fixtures.FixtureRequest() +.. autoclass:: pytest.FixtureRequest() :members: @@ -475,9 +472,9 @@ caplog .. autofunction:: _pytest.logging.caplog() :no-auto-options: - Returns a :class:`_pytest.logging.LogCaptureFixture` instance. + Returns a :class:`pytest.LogCaptureFixture` instance. -.. autoclass:: _pytest.logging.LogCaptureFixture +.. autoclass:: pytest.LogCaptureFixture() :members: @@ -504,9 +501,7 @@ pytester .. versionadded:: 6.2 -.. currentmodule:: _pytest.pytester - -Provides a :class:`Pytester` instance that can be used to run and test pytest itself. +Provides a :class:`~pytest.Pytester` instance that can be used to run and test pytest itself. It provides an empty directory where pytest can be executed in isolation, and contains facilities to write tests, configuration files, and match against expected output. @@ -519,16 +514,16 @@ To use it, include in your topmost ``conftest.py`` file: -.. autoclass:: Pytester() +.. autoclass:: pytest.Pytester() :members: -.. autoclass:: RunResult() +.. autoclass:: _pytest.pytester.RunResult() :members: -.. autoclass:: LineMatcher() +.. autoclass:: _pytest.pytester.LineMatcher() :members: -.. autoclass:: HookRecorder() +.. autoclass:: _pytest.pytester.HookRecorder() :members: .. fixture:: testdir @@ -541,7 +536,7 @@ legacy ``py.path.local`` objects instead when applicable. New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. -.. autoclass:: Testdir() +.. autoclass:: pytest.Testdir() :members: @@ -552,12 +547,10 @@ recwarn **Tutorial**: :ref:`assertwarnings` -.. currentmodule:: _pytest.recwarn - -.. autofunction:: recwarn() +.. autofunction:: _pytest.recwarn.recwarn() :no-auto-options: -.. autoclass:: WarningsRecorder() +.. autoclass:: pytest.WarningsRecorder() :members: Each recorded warning is an instance of :class:`warnings.WarningMessage`. @@ -574,13 +567,11 @@ tmp_path **Tutorial**: :doc:`tmpdir` -.. currentmodule:: _pytest.tmpdir - -.. autofunction:: tmp_path() +.. autofunction:: _pytest.tmpdir.tmp_path() :no-auto-options: -.. fixture:: tmp_path_factory +.. fixture:: _pytest.tmpdir.tmp_path_factory tmp_path_factory ~~~~~~~~~~~~~~~~ @@ -589,12 +580,9 @@ tmp_path_factory .. _`tmp_path_factory factory api`: -``tmp_path_factory`` instances have the following methods: +``tmp_path_factory`` is an instance of :class:`~pytest.TempPathFactory`: -.. currentmodule:: _pytest.tmpdir - -.. automethod:: TempPathFactory.mktemp -.. automethod:: TempPathFactory.getbasetemp +.. autoclass:: pytest.TempPathFactory() .. fixture:: tmpdir @@ -604,9 +592,7 @@ tmpdir **Tutorial**: :doc:`tmpdir` -.. currentmodule:: _pytest.tmpdir - -.. autofunction:: tmpdir() +.. autofunction:: _pytest.tmpdir.tmpdir() :no-auto-options: @@ -619,12 +605,9 @@ tmpdir_factory .. _`tmpdir factory api`: -``tmpdir_factory`` instances have the following methods: - -.. currentmodule:: _pytest.tmpdir +``tmp_path_factory`` is an instance of :class:`~pytest.TempdirFactory`: -.. automethod:: TempdirFactory.mktemp -.. automethod:: TempdirFactory.getbasetemp +.. autoclass:: pytest.TempdirFactory() .. _`hook-reference`: diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 1689b9a410f..03acd03109e 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -25,6 +25,7 @@ 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.main import Session @@ -53,7 +54,7 @@ @final -@attr.s +@attr.s(init=False) class Cache: _cachedir = attr.ib(type=Path, repr=False) _config = attr.ib(type=Config, repr=False) @@ -64,26 +65,52 @@ class Cache: # sub-directory under cache-dir for values created by "set" _CACHE_PREFIX_VALUES = "v" + def __init__( + self, cachedir: Path, config: Config, *, _ispytest: bool = False + ) -> None: + check_ispytest(_ispytest) + self._cachedir = cachedir + self._config = config + @classmethod - def for_config(cls, config: Config) -> "Cache": - cachedir = cls.cache_dir_from_config(config) + def for_config(cls, config: Config, *, _ispytest: bool = False) -> "Cache": + """Create the Cache instance for a Config. + + :meta private: + """ + check_ispytest(_ispytest) + cachedir = cls.cache_dir_from_config(config, _ispytest=True) if config.getoption("cacheclear") and cachedir.is_dir(): - cls.clear_cache(cachedir) - return cls(cachedir, config) + cls.clear_cache(cachedir, _ispytest=True) + return cls(cachedir, config, _ispytest=True) @classmethod - def clear_cache(cls, cachedir: Path) -> None: - """Clear the sub-directories used to hold cached directories and values.""" + def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None: + """Clear the sub-directories used to hold cached directories and values. + + :meta private: + """ + check_ispytest(_ispytest) for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES): d = cachedir / prefix if d.is_dir(): rm_rf(d) @staticmethod - def cache_dir_from_config(config: Config) -> Path: + def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path: + """Get the path to the cache directory for a Config. + + :meta private: + """ + check_ispytest(_ispytest) return resolve_from_str(config.getini("cache_dir"), config.rootpath) - def warn(self, fmt: str, **args: object) -> None: + def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None: + """Issue a cache warning. + + :meta private: + """ + check_ispytest(_ispytest) import warnings from _pytest.warning_types import PytestCacheWarning @@ -152,7 +179,7 @@ def set(self, key: str, value: object) -> None: cache_dir_exists_already = self._cachedir.exists() path.parent.mkdir(exist_ok=True, parents=True) except OSError: - self.warn("could not create cache path {path}", path=path) + self.warn("could not create cache path {path}", path=path, _ispytest=True) return if not cache_dir_exists_already: self._ensure_supporting_files() @@ -160,7 +187,7 @@ def set(self, key: str, value: object) -> None: try: f = path.open("w") except OSError: - self.warn("cache could not write path {path}", path=path) + self.warn("cache could not write path {path}", path=path, _ispytest=True) else: with f: f.write(data) @@ -469,7 +496,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: @hookimpl(tryfirst=True) def pytest_configure(config: Config) -> None: - config.cache = Cache.for_config(config) + config.cache = Cache.for_config(config, _ispytest=True) config.pluginmanager.register(LFPlugin(config), "lfplugin") config.pluginmanager.register(NFPlugin(config), "nfplugin") diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 1c3a2b81959..086302658cb 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -21,6 +21,7 @@ from _pytest.config import Config 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 SubRequest from _pytest.nodes import Collector @@ -826,10 +827,13 @@ def pytest_internalerror(self) -> None: class CaptureFixture(Generic[AnyStr]): - """Object returned by the :py:func:`capsys`, :py:func:`capsysbinary`, - :py:func:`capfd` and :py:func:`capfdbinary` fixtures.""" + """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`, + :fixture:`capfd` and :fixture:`capfdbinary` fixtures.""" - def __init__(self, captureclass, request: SubRequest) -> None: + def __init__( + self, captureclass, request: SubRequest, *, _ispytest: bool = False + ) -> None: + check_ispytest(_ispytest) self.captureclass = captureclass self.request = request self._capture: Optional[MultiCapture[AnyStr]] = None @@ -904,7 +908,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: ``out`` and ``err`` will be ``text`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture[str](SysCapture, request) + capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture @@ -921,7 +925,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, ``out`` and ``err`` will be ``bytes`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request) + capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture @@ -938,7 +942,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: ``out`` and ``err`` will be ``text`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture[str](FDCapture, request) + capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture @@ -955,7 +959,7 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N ``out`` and ``err`` will be ``byte`` objects. """ capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request) + capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 2e9154e8380..19b31d66538 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -8,6 +8,8 @@ :class:`PytestWarning`, or :class:`UnformattedWarning` in case of warnings which need to format their messages. """ +from warnings import warn + from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import UnformattedWarning @@ -59,3 +61,27 @@ STRICT_OPTION = PytestDeprecationWarning( "The --strict option is deprecated, use --strict-markers instead." ) + +PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") + + +# You want to make some `__init__` or function "private". +# +# def my_private_function(some, args): +# ... +# +# Do this: +# +# def my_private_function(some, args, *, _ispytest: bool = False): +# check_ispytest(_ispytest) +# ... +# +# Change all internal/allowed calls to +# +# my_private_function(some, args, _ispytest=True) +# +# All other calls will get the default _ispytest=False and trigger +# the warning (possibly error in the future). +def check_ispytest(ispytest: bool) -> None: + if not ispytest: + warn(PRIVATE, stacklevel=3) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index fd9434a9215..64e8f0e0eee 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -563,7 +563,7 @@ def func() -> None: doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined] node=doctest_item, func=func, cls=None, funcargs=False ) - fixture_request = FixtureRequest(doctest_item) + fixture_request = FixtureRequest(doctest_item, _ispytest=True) fixture_request._fillfixtures() return fixture_request diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index cef998c03b7..273bcafd393 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -49,6 +49,7 @@ from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest from _pytest.deprecated import FILLFUNCARGS from _pytest.deprecated import YIELD_FIXTURE from _pytest.mark import Mark @@ -367,7 +368,7 @@ def _fill_fixtures_impl(function: "Function") -> None: assert function.parent is not None fi = fm.getfixtureinfo(function.parent, function.obj, None) function._fixtureinfo = fi - request = function._request = FixtureRequest(function) + request = function._request = FixtureRequest(function, _ispytest=True) request._fillfixtures() # Prune out funcargs for jstests. newfuncargs = {} @@ -429,7 +430,8 @@ class FixtureRequest: indirectly. """ - def __init__(self, pyfuncitem) -> None: + def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) self._pyfuncitem = pyfuncitem #: Fixture for which this request is being performed. self.fixturename: Optional[str] = None @@ -674,7 +676,9 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: if paramscopenum is not None: scope = scopes[paramscopenum] - subrequest = SubRequest(self, scope, param, param_index, fixturedef) + subrequest = SubRequest( + self, scope, param, param_index, fixturedef, _ispytest=True + ) # Check if a higher-level scoped fixture accesses a lower level one. subrequest._check_scope(argname, self.scope, scope) @@ -751,7 +755,10 @@ def __init__( param, param_index: int, fixturedef: "FixtureDef[object]", + *, + _ispytest: bool = False, ) -> None: + check_ispytest(_ispytest) self._parent_request = request self.fixturename = fixturedef.argname if param is not NOTSET: @@ -769,6 +776,8 @@ def __repr__(self) -> str: return f"" def addfinalizer(self, finalizer: Callable[[], object]) -> None: + """Add finalizer/teardown function to be called after the last test + within the requesting test context finished execution.""" self._fixturedef.addfinalizer(finalizer) def _schedule_finalizers( diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 2f5da8e7a00..2e4847328ab 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -27,6 +27,7 @@ from _pytest.config import hookimpl from _pytest.config import UsageError from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.main import Session @@ -346,7 +347,8 @@ def handleError(self, record: logging.LogRecord) -> None: class LogCaptureFixture: """Provides access and control of log capturing.""" - def __init__(self, item: nodes.Node) -> None: + def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) self._item = item self._initial_handler_level: Optional[int] = None # Dict of log name -> log level. @@ -482,7 +484,7 @@ def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]: * caplog.record_tuples -> list of (logger_name, level, message) tuples * caplog.clear() -> clear captured records and formatted log output string """ - result = LogCaptureFixture(request.node) + result = LogCaptureFixture(request.node, _ispytest=True) yield result result._finalize() diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 158b71b3a52..20ea71edc64 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -48,6 +48,7 @@ from _pytest.config import main from _pytest.config import PytestPluginManager from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.main import Session @@ -454,7 +455,7 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path` fixture but provides methods which aid in testing pytest itself. """ - return Pytester(request, tmp_path_factory) + return Pytester(request, tmp_path_factory, _ispytest=True) @fixture @@ -465,7 +466,7 @@ def testdir(pytester: "Pytester") -> "Testdir": New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. """ - return Testdir(pytester) + return Testdir(pytester, _ispytest=True) @fixture @@ -648,8 +649,13 @@ class TimeoutExpired(Exception): pass def __init__( - self, request: FixtureRequest, tmp_path_factory: TempPathFactory + self, + request: FixtureRequest, + tmp_path_factory: TempPathFactory, + *, + _ispytest: bool = False, ) -> None: + check_ispytest(_ispytest) self._request = request self._mod_collections: WeakKeyDictionary[ Collector, List[Union[Item, Collector]] @@ -1480,7 +1486,7 @@ def assert_contains_lines(self, lines2: Sequence[str]) -> None: @final -@attr.s(repr=False, str=False) +@attr.s(repr=False, str=False, init=False) class Testdir: """ Similar to :class:`Pytester`, but this class works with legacy py.path.local objects instead. @@ -1495,7 +1501,9 @@ class Testdir: TimeoutExpired = Pytester.TimeoutExpired Session = Pytester.Session - _pytester: Pytester = attr.ib() + def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) + self._pytester = pytester @property def tmpdir(self) -> py.path.local: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index a3eaa58238e..e48e7531c19 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1620,7 +1620,7 @@ def from_parent(cls, parent, **kw): # todo: determine sound type limitations def _initrequest(self) -> None: self.funcargs: Dict[str, object] = {} - self._request = fixtures.FixtureRequest(self) + self._request = fixtures.FixtureRequest(self, _ispytest=True) @property def function(self): diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 49f1e590296..58b449114c0 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -16,6 +16,7 @@ from typing import Union from _pytest.compat import final +from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture from _pytest.outcomes import fail @@ -30,7 +31,7 @@ def recwarn() -> Generator["WarningsRecorder", None, None]: See http://docs.python.org/library/warnings.html for information on warning categories. """ - wrec = WarningsRecorder() + wrec = WarningsRecorder(_ispytest=True) with wrec: warnings.simplefilter("default") yield wrec @@ -142,14 +143,14 @@ def warns( msg += ", ".join(sorted(kwargs)) msg += "\nUse context-manager form instead?" raise TypeError(msg) - return WarningsChecker(expected_warning, match_expr=match) + return WarningsChecker(expected_warning, match_expr=match, _ispytest=True) else: func = args[0] if not callable(func): raise TypeError( "{!r} object (type: {}) must be callable".format(func, type(func)) ) - with WarningsChecker(expected_warning): + with WarningsChecker(expected_warning, _ispytest=True): return func(*args[1:], **kwargs) @@ -159,7 +160,8 @@ class WarningsRecorder(warnings.catch_warnings): Adapted from `warnings.catch_warnings`. """ - def __init__(self) -> None: + def __init__(self, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) # Type ignored due to the way typeshed handles warnings.catch_warnings. super().__init__(record=True) # type: ignore[call-arg] self._entered = False @@ -232,8 +234,11 @@ def __init__( Union[Type[Warning], Tuple[Type[Warning], ...]] ] = None, match_expr: Optional[Union[str, Pattern[str]]] = None, + *, + _ispytest: bool = False, ) -> None: - super().__init__() + check_ispytest(_ispytest) + super().__init__(_ispytest=True) msg = "exceptions must be derived from Warning, not %s" if expected_warning is None: diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 4ca1dd6e136..e62d08db5f5 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -14,37 +14,56 @@ from .pathlib import make_numbered_dir_with_cleanup from _pytest.compat import final from _pytest.config import Config +from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch @final -@attr.s +@attr.s(init=False) class TempPathFactory: """Factory for temporary directories under the common base temp directory. The base directory can be configured using the ``--basetemp`` option. """ - _given_basetemp = attr.ib( - type=Optional[Path], - # Use os.path.abspath() to get absolute path instead of resolve() as it - # does not work the same in all platforms (see #4427). - # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012). - # Ignore type because of https://github.com/python/mypy/issues/6172. - converter=attr.converters.optional( - lambda p: Path(os.path.abspath(str(p))) # type: ignore - ), - ) + _given_basetemp = attr.ib(type=Optional[Path]) _trace = attr.ib() - _basetemp = attr.ib(type=Optional[Path], default=None) + _basetemp = attr.ib(type=Optional[Path]) + + def __init__( + self, + given_basetemp: Optional[Path], + trace, + basetemp: Optional[Path] = None, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + if given_basetemp is None: + self._given_basetemp = None + else: + # Use os.path.abspath() to get absolute path instead of resolve() as it + # does not work the same in all platforms (see #4427). + # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012). + self._given_basetemp = Path(os.path.abspath(str(given_basetemp))) + self._trace = trace + self._basetemp = basetemp @classmethod - def from_config(cls, config: Config) -> "TempPathFactory": - """Create a factory according to pytest configuration.""" + def from_config( + cls, config: Config, *, _ispytest: bool = False, + ) -> "TempPathFactory": + """Create a factory according to pytest configuration. + + :meta private: + """ + check_ispytest(_ispytest) return cls( - given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir") + given_basetemp=config.option.basetemp, + trace=config.trace.get("tmpdir"), + _ispytest=True, ) def _ensure_relative_to_basetemp(self, basename: str) -> str: @@ -104,13 +123,19 @@ def getbasetemp(self) -> Path: @final -@attr.s +@attr.s(init=False) class TempdirFactory: """Backward comptibility wrapper that implements :class:``py.path.local`` for :class:``TempPathFactory``.""" _tmppath_factory = attr.ib(type=TempPathFactory) + def __init__( + self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False + ) -> None: + check_ispytest(_ispytest) + self._tmppath_factory = tmppath_factory + def mktemp(self, basename: str, numbered: bool = True) -> py.path.local: """Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object.""" return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve()) @@ -139,8 +164,8 @@ def pytest_configure(config: Config) -> None: to the tmpdir_factory session fixture. """ mp = MonkeyPatch() - tmppath_handler = TempPathFactory.from_config(config) - t = TempdirFactory(tmppath_handler) + tmppath_handler = TempPathFactory.from_config(config, _ispytest=True) + t = TempdirFactory(tmppath_handler, _ispytest=True) config._cleanup.append(mp.undo) mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False) mp.setattr(config, "_tmpdirhandler", t, raising=False) diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index d7a5b22997f..8af095ea8ae 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -3,6 +3,8 @@ from . import collect from _pytest import __version__ from _pytest.assertion import register_assert_rewrite +from _pytest.cacheprovider import Cache +from _pytest.capture import CaptureFixture from _pytest.config import cmdline from _pytest.config import console_main from _pytest.config import ExitCode @@ -14,8 +16,10 @@ from _pytest.fixtures import _fillfuncargs from _pytest.fixtures import fixture from _pytest.fixtures import FixtureLookupError +from _pytest.fixtures import FixtureRequest from _pytest.fixtures import yield_fixture from _pytest.freeze_support import freeze_includes +from _pytest.logging import LogCaptureFixture from _pytest.main import Session from _pytest.mark import MARK_GEN as mark from _pytest.mark import param @@ -28,6 +32,8 @@ from _pytest.outcomes import importorskip from _pytest.outcomes import skip from _pytest.outcomes import xfail +from _pytest.pytester import Pytester +from _pytest.pytester import Testdir from _pytest.python import Class from _pytest.python import Function from _pytest.python import Instance @@ -36,7 +42,10 @@ from _pytest.python_api import approx from _pytest.python_api import raises from _pytest.recwarn import deprecated_call +from _pytest.recwarn import WarningsRecorder from _pytest.recwarn import warns +from _pytest.tmpdir import TempdirFactory +from _pytest.tmpdir import TempPathFactory from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warning_types import PytestCacheWarning from _pytest.warning_types import PytestCollectionWarning @@ -53,6 +62,8 @@ "__version__", "_fillfuncargs", "approx", + "Cache", + "CaptureFixture", "Class", "cmdline", "collect", @@ -65,6 +76,7 @@ "File", "fixture", "FixtureLookupError", + "FixtureRequest", "freeze_includes", "Function", "hookimpl", @@ -72,6 +84,7 @@ "importorskip", "Instance", "Item", + "LogCaptureFixture", "main", "mark", "Module", @@ -84,6 +97,7 @@ "PytestConfigWarning", "PytestDeprecationWarning", "PytestExperimentalApiWarning", + "Pytester", "PytestUnhandledCoroutineWarning", "PytestUnknownMarkWarning", "PytestWarning", @@ -92,7 +106,11 @@ "Session", "set_trace", "skip", + "TempPathFactory", + "Testdir", + "TempdirFactory", "UsageError", + "WarningsRecorder", "warns", "xfail", "yield_fixture", diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 0d1b58ad16a..d213414ee45 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -123,3 +123,17 @@ def test_yield_fixture_is_deprecated() -> None: @pytest.yield_fixture def fix(): assert False + + +def test_private_is_deprecated() -> None: + class PrivateInit: + def __init__(self, foo: int, *, _ispytest: bool = False) -> None: + deprecated.check_ispytest(_ispytest) + + with pytest.warns( + pytest.PytestDeprecationWarning, match="private pytest class or function" + ): + PrivateInit(10) + + # Doesn't warn. + PrivateInit(10, _ispytest=True) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index a5637b47642..94547dd245c 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -621,7 +621,7 @@ def something(request): pass def test_func(something): pass """ ) - req = fixtures.FixtureRequest(item) + req = fixtures.FixtureRequest(item, _ispytest=True) assert req.function == item.obj assert req.keywords == item.keywords assert hasattr(req.module, "test_func") @@ -661,7 +661,9 @@ def test_method(self, something): ) (item1,) = testdir.genitems([modcol]) assert item1.name == "test_method" - arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs + arg2fixturedefs = fixtures.FixtureRequest( + item1, _ispytest=True + )._arg2fixturedefs assert len(arg2fixturedefs) == 1 assert arg2fixturedefs["something"][0].argname == "something" @@ -910,7 +912,7 @@ def test_second(): def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") (item,) = testdir.genitems([modcol]) - req = fixtures.FixtureRequest(item) + req = fixtures.FixtureRequest(item, _ispytest=True) assert req.fspath == modcol.fspath def test_request_fixturenames(self, testdir): @@ -1052,7 +1054,7 @@ def test_func2(self, something): pass """ ) - req1 = fixtures.FixtureRequest(item1) + req1 = fixtures.FixtureRequest(item1, _ispytest=True) assert "xfail" not in item1.keywords req1.applymarker(pytest.mark.xfail) assert "xfail" in item1.keywords @@ -3882,7 +3884,7 @@ def test_func(m1): """ ) items, _ = testdir.inline_genitems() - request = FixtureRequest(items[0]) + request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "m1 f1".split() def test_func_closure_with_native_fixtures(self, testdir, monkeypatch) -> None: @@ -3928,7 +3930,7 @@ def test_foo(f1, p1, m1, f2, s1): pass """ ) items, _ = testdir.inline_genitems() - request = FixtureRequest(items[0]) + request = FixtureRequest(items[0], _ispytest=True) # order of fixtures based on their scope and position in the parameter list assert ( request.fixturenames == "s1 my_tmpdir_factory p1 m1 f1 f2 my_tmpdir".split() @@ -3954,7 +3956,7 @@ def test_func(f1, m1): """ ) items, _ = testdir.inline_genitems() - request = FixtureRequest(items[0]) + request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "m1 f1".split() def test_func_closure_scopes_reordered(self, testdir): @@ -3987,7 +3989,7 @@ def test_func(self, f2, f1, c1, m1, s1): """ ) items, _ = testdir.inline_genitems() - request = FixtureRequest(items[0]) + request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "s1 m1 c1 f2 f1".split() def test_func_closure_same_scope_closer_root_first(self, testdir): @@ -4027,7 +4029,7 @@ def test_func(m_test, f1): } ) items, _ = testdir.inline_genitems() - request = FixtureRequest(items[0]) + request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "p_sub m_conf m_sub m_test f1".split() def test_func_closure_all_scopes_complex(self, testdir): @@ -4071,7 +4073,7 @@ def test_func(self, f2, f1, m2): """ ) items, _ = testdir.inline_genitems() - request = FixtureRequest(items[0]) + request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split() def test_multiple_packages(self, testdir): diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 37253b8b5db..ccc7304b02a 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -1156,7 +1156,7 @@ def test_gitignore(testdir): from _pytest.cacheprovider import Cache config = testdir.parseconfig() - cache = Cache.for_config(config) + cache = Cache.for_config(config, _ispytest=True) cache.set("foo", "bar") msg = "# Created by pytest automatically.\n*\n" gitignore_path = cache._cachedir.joinpath(".gitignore") @@ -1178,7 +1178,7 @@ def test_does_not_create_boilerplate_in_existing_dirs(testdir): """ ) config = testdir.parseconfig() - cache = Cache.for_config(config) + cache = Cache.for_config(config, _ispytest=True) cache.set("foo", "bar") assert os.path.isdir("v") # cache contents @@ -1192,7 +1192,7 @@ def test_cachedir_tag(testdir): from _pytest.cacheprovider import CACHEDIR_TAG_CONTENT config = testdir.parseconfig() - cache = Cache.for_config(config) + cache = Cache.for_config(config, _ispytest=True) cache.set("foo", "bar") cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG") assert cachedir_tag_path.read_bytes() == CACHEDIR_TAG_CONTENT diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index f61f8586f9c..05970061ed3 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -28,7 +28,7 @@ def test_method(recwarn): class TestWarningsRecorderChecker: def test_recording(self) -> None: - rec = WarningsRecorder() + rec = WarningsRecorder(_ispytest=True) with rec: assert not rec.list warnings.warn_explicit("hello", UserWarning, "xyz", 13) @@ -45,7 +45,7 @@ def test_recording(self) -> None: def test_warn_stacklevel(self) -> None: """#4243""" - rec = WarningsRecorder() + rec = WarningsRecorder(_ispytest=True) with rec: warnings.warn("test", DeprecationWarning, 2) @@ -53,21 +53,21 @@ def test_typechecking(self) -> None: from _pytest.recwarn import WarningsChecker with pytest.raises(TypeError): - WarningsChecker(5) # type: ignore + WarningsChecker(5, _ispytest=True) # type: ignore[arg-type] with pytest.raises(TypeError): - WarningsChecker(("hi", RuntimeWarning)) # type: ignore + WarningsChecker(("hi", RuntimeWarning), _ispytest=True) # type: ignore[arg-type] with pytest.raises(TypeError): - WarningsChecker([DeprecationWarning, RuntimeWarning]) # type: ignore + WarningsChecker([DeprecationWarning, RuntimeWarning], _ispytest=True) # type: ignore[arg-type] def test_invalid_enter_exit(self) -> None: # wrap this test in WarningsRecorder to ensure warning state gets reset - with WarningsRecorder(): + with WarningsRecorder(_ispytest=True): with pytest.raises(RuntimeError): - rec = WarningsRecorder() + rec = WarningsRecorder(_ispytest=True) rec.__exit__(None, None, None) # can't exit before entering with pytest.raises(RuntimeError): - rec = WarningsRecorder() + rec = WarningsRecorder(_ispytest=True) with rec: with rec: pass # can't enter twice diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index bd6e7b968b0..1df0e2207b3 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -48,7 +48,9 @@ def option(self): class TestTempdirHandler: def test_mktemp(self, tmp_path): config = cast(Config, FakeConfig(tmp_path)) - t = TempdirFactory(TempPathFactory.from_config(config)) + t = TempdirFactory( + TempPathFactory.from_config(config, _ispytest=True), _ispytest=True + ) tmp = t.mktemp("world") assert tmp.relto(t.getbasetemp()) == "world0" tmp = t.mktemp("this") @@ -61,7 +63,7 @@ def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch): """#4425""" monkeypatch.chdir(tmp_path) config = cast(Config, FakeConfig("hello")) - t = TempPathFactory.from_config(config) + t = TempPathFactory.from_config(config, _ispytest=True) assert t.getbasetemp().resolve() == (tmp_path / "hello").resolve() From 701ff1f5a1500f3834343a8aa0031fbcbad58aac Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 17 Oct 2020 18:55:30 +0300 Subject: [PATCH 781/823] ci: only deploy to PyPI on X.Y.Z{,rcN} tags We want to reserve other tags for our own purposes without it creating a release. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5e9367a5d69..563c47638e5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,8 @@ on: - master - "[0-9]+.[0-9]+.x" tags: - - "*" + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" pull_request: branches: From f6b682ad49c5db1b374193a4b2052415d6d341b1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 17 Oct 2020 19:08:48 +0300 Subject: [PATCH 782/823] RELEASING: start new dev cycle by tagging MAJOR.{MINOR+1}.0.dev0 in master This is needed so setuptools-scm in master shows an accurate version. In particular, higher than the stable branch. --- RELEASING.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASING.rst b/RELEASING.rst index 9ff95be92ed..9ec2b069c68 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -122,6 +122,14 @@ Both automatic and manual processes described above follow the same steps from t #. Open a PR for ``cherry-pick-release`` and merge it once CI passes. No need to wait for approvals if there were no conflicts on the previous step. +#. For major and minor releases, tag the release cherry-pick merge commit in master with + a dev tag for the next feature release:: + + git checkout master + git pull + git tag MAJOR.{MINOR+1}.0.dev0 + git push git@github.com:pytest-dev/pytest.git MAJOR.{MINOR+1}.0.dev0 + #. Send an email announcement with the contents from:: doc/en/announce/release-.rst From 25e4dd0d2cac93d7fd65afb1f002a521909f7e21 Mon Sep 17 00:00:00 2001 From: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Date: Fri, 13 Nov 2020 20:00:44 +0000 Subject: [PATCH 783/823] Fix scope to accomodate requested changes --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index be7ac82dd8c..23e7c996688 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,7 @@ issue/ env/ .env/ .venv/ -pythonenv* +/pythonenv*/ 3rdparty/ .tox .cache From 1d532da49ec7e25ff9d78509a61c3aa82a29b482 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 9 Nov 2020 15:07:51 +0200 Subject: [PATCH 784/823] assertion/rewrite: write pyc's according to PEP-552 on Python>=3.7 Python 3.7 changes the pyc format by adding a flags byte. Even though it is not necessary for us to match it, it is nice to be able to read pyc files we emit for debugging the rewriter. Update our custom pyc files to use that format. We write flags==0 meaning we still use the mtime+size format rather the newer hash format. --- changelog/8014.trivial.rst | 2 ++ src/_pytest/assertion/rewrite.py | 34 +++++++++++++++------ testing/test_assertrewrite.py | 51 +++++++++++++++++++++++++++++++- 3 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 changelog/8014.trivial.rst diff --git a/changelog/8014.trivial.rst b/changelog/8014.trivial.rst new file mode 100644 index 00000000000..3b9fb7bc271 --- /dev/null +++ b/changelog/8014.trivial.rst @@ -0,0 +1,2 @@ +`.pyc` files created by pytest's assertion rewriting now conform to the newer PEP-552 format on Python>=3.7. +(These files are internal and only interpreted by pytest itself.) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 649726727c5..805d4c8b35b 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -281,12 +281,16 @@ def _write_pyc_fp( ) -> None: # Technically, we don't have to have the same pyc format as # (C)Python, since these "pycs" should never be seen by builtin - # import. However, there's little reason deviate. + # import. However, there's little reason to deviate. fp.write(importlib.util.MAGIC_NUMBER) + # https://www.python.org/dev/peps/pep-0552/ + if sys.version_info >= (3, 7): + flags = b"\x00\x00\x00\x00" + fp.write(flags) # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) mtime = int(source_stat.st_mtime) & 0xFFFFFFFF size = source_stat.st_size & 0xFFFFFFFF - # "= (3, 7) try: stat_result = os.stat(os.fspath(source)) mtime = int(stat_result.st_mtime) size = stat_result.st_size - data = fp.read(12) + data = fp.read(16 if has_flags else 12) except OSError as e: trace(f"_read_pyc({source}): OSError {e}") return None # Check for invalid or out of date pyc file. - if ( - len(data) != 12 - or data[:4] != importlib.util.MAGIC_NUMBER - or struct.unpack(" None: py_compile.compile(str(source), str(pyc)) contents = pyc.read_bytes() - strip_bytes = 20 # header is around 8 bytes, strip a little more + strip_bytes = 20 # header is around 16 bytes, strip a little more assert len(contents) > strip_bytes pyc.write_bytes(contents[:strip_bytes]) assert _read_pyc(source, pyc) is None # no error + @pytest.mark.skipif( + sys.version_info < (3, 7), reason="Only the Python 3.7 format for simplicity" + ) + def test_read_pyc_more_invalid(self, tmp_path: Path) -> None: + from _pytest.assertion.rewrite import _read_pyc + + source = tmp_path / "source.py" + pyc = tmp_path / "source.pyc" + + source_bytes = b"def test(): pass\n" + source.write_bytes(source_bytes) + + magic = importlib.util.MAGIC_NUMBER + + flags = b"\x00\x00\x00\x00" + + mtime = b"\x58\x3c\xb0\x5f" + mtime_int = int.from_bytes(mtime, "little") + os.utime(source, (mtime_int, mtime_int)) + + size = len(source_bytes).to_bytes(4, "little") + + code = marshal.dumps(compile(source_bytes, str(source), "exec")) + + # Good header. + pyc.write_bytes(magic + flags + mtime + size + code) + assert _read_pyc(source, pyc, print) is not None + + # Too short. + pyc.write_bytes(magic + flags + mtime) + assert _read_pyc(source, pyc, print) is None + + # Bad magic. + pyc.write_bytes(b"\x12\x34\x56\x78" + flags + mtime + size + code) + assert _read_pyc(source, pyc, print) is None + + # Unsupported flags. + pyc.write_bytes(magic + b"\x00\xff\x00\x00" + mtime + size + code) + assert _read_pyc(source, pyc, print) is None + + # Bad mtime. + pyc.write_bytes(magic + flags + b"\x58\x3d\xb0\x5f" + size + code) + assert _read_pyc(source, pyc, print) is None + + # Bad size. + pyc.write_bytes(magic + flags + mtime + b"\x99\x00\x00\x00" + code) + assert _read_pyc(source, pyc, print) is None + def test_reload_is_same_and_reloads(self, pytester: Pytester) -> None: """Reloading a (collected) module after change picks up the change.""" pytester.makeini( From 3a899ced76be93dbf10eb8935763f4724e73bd85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 03:15:18 +0000 Subject: [PATCH 785/823] build(deps): bump pytest-html in /testing/plugins_integration Bumps [pytest-html](https://github.com/pytest-dev/pytest-html) from 2.1.1 to 3.0.0. - [Release notes](https://github.com/pytest-dev/pytest-html/releases) - [Changelog](https://github.com/pytest-dev/pytest-html/blob/master/CHANGES.rst) - [Commits](https://github.com/pytest-dev/pytest-html/compare/v2.1.1...v3.0.0) 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 78eba3155a6..4ffa8f7a1e1 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -5,7 +5,7 @@ pytest-bdd==4.0.1 pytest-cov==2.10.1 pytest-django==4.1.0 pytest-flakes==4.0.2 -pytest-html==2.1.1 +pytest-html==3.0.0 pytest-mock==3.3.1 pytest-rerunfailures==9.1.1 pytest-sugar==0.9.4 From 6fe9d2fb9f678835d5f1366b89ce7b05489957b8 Mon Sep 17 00:00:00 2001 From: Garvit Shubham Date: Mon, 9 Nov 2020 09:53:31 +0530 Subject: [PATCH 786/823] testing: convert test_{conftest,recwarn,tmpdir} to pytester --- testing/test_conftest.py | 298 +++++++++++++++++++++------------------ testing/test_recwarn.py | 13 +- testing/test_tmpdir.py | 87 ++++++------ 3 files changed, 213 insertions(+), 185 deletions(-) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index db56702041b..638321728d7 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,31 +1,42 @@ +import argparse import os import textwrap from pathlib import Path +from typing import cast +from typing import Dict +from typing import List +from typing import Optional import py import pytest from _pytest.config import ExitCode from _pytest.config import PytestPluginManager +from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import symlink_or_skip +from _pytest.pytester import Pytester +from _pytest.pytester import Testdir -def ConftestWithSetinitial(path): +def ConftestWithSetinitial(path) -> PytestPluginManager: conftest = PytestPluginManager() conftest_setinitial(conftest, [path]) return conftest -def conftest_setinitial(conftest, args, confcutdir=None): +def conftest_setinitial( + conftest: PytestPluginManager, args, confcutdir: Optional[py.path.local] = None +) -> None: class Namespace: - def __init__(self): + def __init__(self) -> None: self.file_or_dir = args self.confcutdir = str(confcutdir) self.noconftest = False self.pyargs = False self.importmode = "prepend" - conftest._set_initial_conftests(Namespace()) + namespace = cast(argparse.Namespace, Namespace()) + conftest._set_initial_conftests(namespace) @pytest.mark.usefixtures("_sys_snapshot") @@ -82,28 +93,29 @@ def test_value_access_with_confmod(self, basedir): assert path.purebasename.startswith("conftest") -def test_conftest_in_nonpkg_with_init(tmpdir, _sys_snapshot): - tmpdir.ensure("adir-1.0/conftest.py").write("a=1 ; Directory = 3") - tmpdir.ensure("adir-1.0/b/conftest.py").write("b=2 ; a = 1.5") - tmpdir.ensure("adir-1.0/b/__init__.py") - tmpdir.ensure("adir-1.0/__init__.py") - ConftestWithSetinitial(tmpdir.join("adir-1.0", "b")) +def test_conftest_in_nonpkg_with_init(tmp_path: Path, _sys_snapshot) -> None: + tmp_path.joinpath("adir-1.0/b").mkdir(parents=True) + tmp_path.joinpath("adir-1.0/conftest.py").write_text("a=1 ; Directory = 3") + tmp_path.joinpath("adir-1.0/b/conftest.py").write_text("b=2 ; a = 1.5") + tmp_path.joinpath("adir-1.0/b/__init__.py").touch() + tmp_path.joinpath("adir-1.0/__init__.py").touch() + ConftestWithSetinitial(tmp_path.joinpath("adir-1.0", "b")) -def test_doubledash_considered(testdir): +def test_doubledash_considered(testdir: Testdir) -> None: conf = testdir.mkdir("--option") - conf.ensure("conftest.py") + conf.join("conftest.py").ensure() conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.basename, conf.basename]) - values = conftest._getconftestmodules(conf, importmode="prepend") + values = conftest._getconftestmodules(py.path.local(conf), importmode="prepend") assert len(values) == 1 -def test_issue151_load_all_conftests(testdir): +def test_issue151_load_all_conftests(pytester: Pytester) -> None: names = "code proj src".split() for name in names: - p = testdir.mkdir(name) - p.ensure("conftest.py") + p = pytester.mkdir(name) + p.joinpath("conftest.py").touch() conftest = PytestPluginManager() conftest_setinitial(conftest, names) @@ -111,9 +123,9 @@ def test_issue151_load_all_conftests(testdir): assert len(d) == len(names) -def test_conftest_global_import(testdir): - testdir.makeconftest("x=3") - p = testdir.makepyfile( +def test_conftest_global_import(pytester: Pytester) -> None: + pytester.makeconftest("x=3") + p = pytester.makepyfile( """ import py, pytest from _pytest.config import PytestPluginManager @@ -131,11 +143,11 @@ def test_conftest_global_import(testdir): assert conftest is mod2, (conftest, mod) """ ) - res = testdir.runpython(p) + res = pytester.runpython(p) assert res.ret == 0 -def test_conftestcutdir(testdir): +def test_conftestcutdir(testdir: Testdir) -> None: conf = testdir.makeconftest("") p = testdir.mkdir("x") conftest = PytestPluginManager() @@ -144,7 +156,7 @@ def test_conftestcutdir(testdir): assert len(values) == 0 values = conftest._getconftestmodules(conf.dirpath(), importmode="prepend") assert len(values) == 0 - assert conf not in conftest._conftestpath2mod + assert Path(conf) not in conftest._conftestpath2mod # but we can still import a conftest directly conftest._importconftest(conf, importmode="prepend") values = conftest._getconftestmodules(conf.dirpath(), importmode="prepend") @@ -155,22 +167,25 @@ def test_conftestcutdir(testdir): assert values[0].__file__.startswith(str(conf)) -def test_conftestcutdir_inplace_considered(testdir): - conf = testdir.makeconftest("") +def test_conftestcutdir_inplace_considered(pytester: Pytester) -> None: + conf = pytester.makeconftest("") conftest = PytestPluginManager() - conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath()) - values = conftest._getconftestmodules(conf.dirpath(), importmode="prepend") + conftest_setinitial(conftest, [conf.parent], confcutdir=py.path.local(conf.parent)) + values = conftest._getconftestmodules( + py.path.local(conf.parent), importmode="prepend" + ) assert len(values) == 1 assert values[0].__file__.startswith(str(conf)) @pytest.mark.parametrize("name", "test tests whatever .dotdir".split()) -def test_setinitial_conftest_subdirs(testdir, name): - sub = testdir.mkdir(name) - subconftest = sub.ensure("conftest.py") +def test_setinitial_conftest_subdirs(pytester: Pytester, name: str) -> None: + sub = pytester.mkdir(name) + subconftest = sub.joinpath("conftest.py") + subconftest.touch() conftest = PytestPluginManager() - conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) - key = Path(str(subconftest)).resolve() + conftest_setinitial(conftest, [sub.parent], confcutdir=py.path.local(pytester.path)) + key = subconftest.resolve() if name not in ("whatever", ".dotdir"): assert key in conftest._conftestpath2mod assert len(conftest._conftestpath2mod) == 1 @@ -179,10 +194,10 @@ def test_setinitial_conftest_subdirs(testdir, name): assert len(conftest._conftestpath2mod) == 0 -def test_conftest_confcutdir(testdir): - testdir.makeconftest("assert 0") - x = testdir.mkdir("x") - x.join("conftest.py").write( +def test_conftest_confcutdir(pytester: Pytester) -> None: + pytester.makeconftest("assert 0") + x = pytester.mkdir("x") + x.joinpath("conftest.py").write_text( textwrap.dedent( """\ def pytest_addoption(parser): @@ -190,12 +205,12 @@ def pytest_addoption(parser): """ ) ) - result = testdir.runpytest("-h", "--confcutdir=%s" % x, x) + result = pytester.runpytest("-h", "--confcutdir=%s" % x, x) result.stdout.fnmatch_lines(["*--xyz*"]) result.stdout.no_fnmatch_line("*warning: could not load initial*") -def test_conftest_symlink(testdir): +def test_conftest_symlink(pytester: Pytester) -> None: """`conftest.py` discovery follows normal path resolution and does not resolve symlinks.""" # Structure: # /real @@ -208,11 +223,12 @@ def test_conftest_symlink(testdir): # /symlinktests -> /real/app/tests (running at symlinktests should fail) # /symlink -> /real (running at /symlink should work) - real = testdir.tmpdir.mkdir("real") - realtests = real.mkdir("app").mkdir("tests") - symlink_or_skip(realtests, testdir.tmpdir.join("symlinktests")) - symlink_or_skip(real, testdir.tmpdir.join("symlink")) - testdir.makepyfile( + real = pytester.mkdir("real") + realtests = real.joinpath("app/tests") + realtests.mkdir(parents=True) + symlink_or_skip(realtests, pytester.path.joinpath("symlinktests")) + symlink_or_skip(real, pytester.path.joinpath("symlink")) + pytester.makepyfile( **{ "real/app/tests/test_foo.py": "def test1(fixture): pass", "real/conftest.py": textwrap.dedent( @@ -230,19 +246,19 @@ def fixture(): ) # Should fail because conftest cannot be found from the link structure. - result = testdir.runpytest("-vs", "symlinktests") + result = pytester.runpytest("-vs", "symlinktests") result.stdout.fnmatch_lines(["*fixture 'fixture' not found*"]) assert result.ret == ExitCode.TESTS_FAILED # Should not cause "ValueError: Plugin already registered" (#4174). - result = testdir.runpytest("-vs", "symlink") + result = pytester.runpytest("-vs", "symlink") assert result.ret == ExitCode.OK -def test_conftest_symlink_files(testdir): +def test_conftest_symlink_files(pytester: Pytester) -> None: """Symlinked conftest.py are found when pytest is executed in a directory with symlinked files.""" - real = testdir.tmpdir.mkdir("real") + real = pytester.mkdir("real") source = { "app/test_foo.py": "def test1(fixture): pass", "app/__init__.py": "", @@ -258,16 +274,16 @@ def fixture(): """ ), } - testdir.makepyfile(**{"real/%s" % k: v for k, v in source.items()}) + pytester.makepyfile(**{"real/%s" % k: v for k, v in source.items()}) # Create a build directory that contains symlinks to actual files # but doesn't symlink actual directories. - build = testdir.tmpdir.mkdir("build") - build.mkdir("app") + build = pytester.mkdir("build") + build.joinpath("app").mkdir() for f in source: - symlink_or_skip(real.join(f), build.join(f)) - build.chdir() - result = testdir.runpytest("-vs", "app/test_foo.py") + symlink_or_skip(real.joinpath(f), build.joinpath(f)) + os.chdir(build) + result = pytester.runpytest("-vs", "app/test_foo.py") result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"]) assert result.ret == ExitCode.OK @@ -276,39 +292,39 @@ def fixture(): os.path.normcase("x") != os.path.normcase("X"), reason="only relevant for case insensitive file systems", ) -def test_conftest_badcase(testdir): +def test_conftest_badcase(pytester: Pytester) -> None: """Check conftest.py loading when directory casing is wrong (#5792).""" - testdir.tmpdir.mkdir("JenkinsRoot").mkdir("test") + pytester.path.joinpath("JenkinsRoot/test").mkdir(parents=True) source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""} - testdir.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()}) + pytester.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()}) - testdir.tmpdir.join("jenkinsroot/test").chdir() - result = testdir.runpytest() + os.chdir(pytester.path.joinpath("jenkinsroot/test")) + result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED -def test_conftest_uppercase(testdir): +def test_conftest_uppercase(pytester: Pytester) -> None: """Check conftest.py whose qualified name contains uppercase characters (#5819)""" source = {"__init__.py": "", "Foo/conftest.py": "", "Foo/__init__.py": ""} - testdir.makepyfile(**source) + pytester.makepyfile(**source) - testdir.tmpdir.chdir() - result = testdir.runpytest() + os.chdir(pytester.path) + result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED -def test_no_conftest(testdir): - testdir.makeconftest("assert 0") - result = testdir.runpytest("--noconftest") +def test_no_conftest(pytester: Pytester) -> None: + pytester.makeconftest("assert 0") + result = pytester.runpytest("--noconftest") assert result.ret == ExitCode.NO_TESTS_COLLECTED - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == ExitCode.USAGE_ERROR -def test_conftest_existing_junitxml(testdir): - x = testdir.mkdir("tests") - x.join("conftest.py").write( +def test_conftest_existing_junitxml(pytester: Pytester) -> None: + x = pytester.mkdir("tests") + x.joinpath("conftest.py").write_text( textwrap.dedent( """\ def pytest_addoption(parser): @@ -316,12 +332,12 @@ def pytest_addoption(parser): """ ) ) - testdir.makefile(ext=".xml", junit="") # Writes junit.xml - result = testdir.runpytest("-h", "--junitxml", "junit.xml") + pytester.makefile(ext=".xml", junit="") # Writes junit.xml + result = pytester.runpytest("-h", "--junitxml", "junit.xml") result.stdout.fnmatch_lines(["*--xyz*"]) -def test_conftest_import_order(testdir, monkeypatch): +def test_conftest_import_order(testdir: Testdir, monkeypatch: MonkeyPatch) -> None: ct1 = testdir.makeconftest("") sub = testdir.mkdir("sub") ct2 = sub.join("conftest.py") @@ -333,16 +349,21 @@ def impct(p, importmode): conftest = PytestPluginManager() conftest._confcutdir = testdir.tmpdir monkeypatch.setattr(conftest, "_importconftest", impct) - assert conftest._getconftestmodules(sub, importmode="prepend") == [ct1, ct2] + mods = cast( + List[py.path.local], + conftest._getconftestmodules(py.path.local(sub), importmode="prepend"), + ) + expected = [ct1, ct2] + assert mods == expected -def test_fixture_dependency(testdir): - ct1 = testdir.makeconftest("") - ct1 = testdir.makepyfile("__init__.py") - ct1.write("") - sub = testdir.mkdir("sub") - sub.join("__init__.py").write("") - sub.join("conftest.py").write( +def test_fixture_dependency(pytester: Pytester) -> None: + ct1 = pytester.makeconftest("") + ct1 = pytester.makepyfile("__init__.py") + ct1.write_text("") + sub = pytester.mkdir("sub") + sub.joinpath("__init__.py").write_text("") + sub.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -361,9 +382,10 @@ def bar(foo): """ ) ) - subsub = sub.mkdir("subsub") - subsub.join("__init__.py").write("") - subsub.join("test_bar.py").write( + subsub = sub.joinpath("subsub") + subsub.mkdir() + subsub.joinpath("__init__.py").write_text("") + subsub.joinpath("test_bar.py").write_text( textwrap.dedent( """\ import pytest @@ -377,13 +399,13 @@ def test_event_fixture(bar): """ ) ) - result = testdir.runpytest("sub") + result = pytester.runpytest("sub") result.stdout.fnmatch_lines(["*1 passed*"]) -def test_conftest_found_with_double_dash(testdir): - sub = testdir.mkdir("sub") - sub.join("conftest.py").write( +def test_conftest_found_with_double_dash(pytester: Pytester) -> None: + sub = pytester.mkdir("sub") + sub.joinpath("conftest.py").write_text( textwrap.dedent( """\ def pytest_addoption(parser): @@ -391,9 +413,9 @@ def pytest_addoption(parser): """ ) ) - p = sub.join("test_hello.py") - p.write("def test_hello(): pass") - result = testdir.runpytest(str(p) + "::test_hello", "-h") + p = sub.joinpath("test_hello.py") + p.write_text("def test_hello(): pass") + result = pytester.runpytest(str(p) + "::test_hello", "-h") result.stdout.fnmatch_lines( """ *--hello-world* @@ -402,13 +424,13 @@ def pytest_addoption(parser): class TestConftestVisibility: - def _setup_tree(self, testdir): # for issue616 + def _setup_tree(self, pytester: Pytester) -> Dict[str, Path]: # for issue616 # example mostly taken from: # https://mail.python.org/pipermail/pytest-dev/2014-September/002617.html - runner = testdir.mkdir("empty") - package = testdir.mkdir("package") + runner = pytester.mkdir("empty") + package = pytester.mkdir("package") - package.join("conftest.py").write( + package.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -418,7 +440,7 @@ def fxtr(): """ ) ) - package.join("test_pkgroot.py").write( + package.joinpath("test_pkgroot.py").write_text( textwrap.dedent( """\ def test_pkgroot(fxtr): @@ -427,9 +449,10 @@ def test_pkgroot(fxtr): ) ) - swc = package.mkdir("swc") - swc.join("__init__.py").ensure() - swc.join("conftest.py").write( + swc = package.joinpath("swc") + swc.mkdir() + swc.joinpath("__init__.py").touch() + swc.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -439,7 +462,7 @@ def fxtr(): """ ) ) - swc.join("test_with_conftest.py").write( + swc.joinpath("test_with_conftest.py").write_text( textwrap.dedent( """\ def test_with_conftest(fxtr): @@ -448,9 +471,10 @@ def test_with_conftest(fxtr): ) ) - snc = package.mkdir("snc") - snc.join("__init__.py").ensure() - snc.join("test_no_conftest.py").write( + snc = package.joinpath("snc") + snc.mkdir() + snc.joinpath("__init__.py").touch() + snc.joinpath("test_no_conftest.py").write_text( textwrap.dedent( """\ def test_no_conftest(fxtr): @@ -460,9 +484,8 @@ def test_no_conftest(fxtr): ) ) print("created directory structure:") - tmppath = Path(str(testdir.tmpdir)) - for x in tmppath.rglob(""): - print(" " + str(x.relative_to(tmppath))) + for x in pytester.path.rglob(""): + print(" " + str(x.relative_to(pytester.path))) return {"runner": runner, "package": package, "swc": swc, "snc": snc} @@ -494,29 +517,32 @@ def test_no_conftest(fxtr): ], ) def test_parsefactories_relative_node_ids( - self, testdir, chdir, testarg, expect_ntests_passed - ): + self, pytester: Pytester, chdir: str, testarg: str, expect_ntests_passed: int + ) -> None: """#616""" - dirs = self._setup_tree(testdir) - print("pytest run in cwd: %s" % (dirs[chdir].relto(testdir.tmpdir))) + dirs = self._setup_tree(pytester) + print("pytest run in cwd: %s" % (dirs[chdir].relative_to(pytester.path))) print("pytestarg : %s" % (testarg)) print("expected pass : %s" % (expect_ntests_passed)) - with dirs[chdir].as_cwd(): - reprec = testdir.inline_run(testarg, "-q", "--traceconfig") - reprec.assertoutcome(passed=expect_ntests_passed) + os.chdir(dirs[chdir]) + reprec = pytester.inline_run(testarg, "-q", "--traceconfig") + reprec.assertoutcome(passed=expect_ntests_passed) @pytest.mark.parametrize( "confcutdir,passed,error", [(".", 2, 0), ("src", 1, 1), (None, 1, 1)] ) -def test_search_conftest_up_to_inifile(testdir, confcutdir, passed, error): +def test_search_conftest_up_to_inifile( + pytester: Pytester, confcutdir: str, passed: int, error: int +) -> None: """Test that conftest files are detected only up to an ini file, unless an explicit --confcutdir option is given. """ - root = testdir.tmpdir - src = root.join("src").ensure(dir=1) - src.join("pytest.ini").write("[pytest]") - src.join("conftest.py").write( + root = pytester.path + src = root.joinpath("src") + src.mkdir() + src.joinpath("pytest.ini").write_text("[pytest]") + src.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -525,7 +551,7 @@ def fix1(): pass """ ) ) - src.join("test_foo.py").write( + src.joinpath("test_foo.py").write_text( textwrap.dedent( """\ def test_1(fix1): @@ -535,7 +561,7 @@ def test_2(out_of_reach): """ ) ) - root.join("conftest.py").write( + root.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @@ -547,8 +573,8 @@ def out_of_reach(): pass args = [str(src)] if confcutdir: - args = ["--confcutdir=%s" % root.join(confcutdir)] - result = testdir.runpytest(*args) + args = ["--confcutdir=%s" % root.joinpath(confcutdir)] + result = pytester.runpytest(*args) match = "" if passed: match += "*%d passed*" % passed @@ -557,8 +583,8 @@ def out_of_reach(): pass result.stdout.fnmatch_lines(match) -def test_issue1073_conftest_special_objects(testdir): - testdir.makeconftest( +def test_issue1073_conftest_special_objects(pytester: Pytester) -> None: + pytester.makeconftest( """\ class DontTouchMe(object): def __getattr__(self, x): @@ -567,38 +593,38 @@ def __getattr__(self, x): x = DontTouchMe() """ ) - testdir.makepyfile( + pytester.makepyfile( """\ def test_some(): pass """ ) - res = testdir.runpytest() + res = pytester.runpytest() assert res.ret == 0 -def test_conftest_exception_handling(testdir): - testdir.makeconftest( +def test_conftest_exception_handling(pytester: Pytester) -> None: + pytester.makeconftest( """\ raise ValueError() """ ) - testdir.makepyfile( + pytester.makepyfile( """\ def test_some(): pass """ ) - res = testdir.runpytest() + res = pytester.runpytest() assert res.ret == 4 assert "raise ValueError()" in [line.strip() for line in res.errlines] -def test_hook_proxy(testdir): +def test_hook_proxy(pytester: Pytester) -> None: """Session's gethookproxy() would cache conftests incorrectly (#2016). It was decided to remove the cache altogether. """ - testdir.makepyfile( + pytester.makepyfile( **{ "root/demo-0/test_foo1.py": "def test1(): pass", "root/demo-a/test_foo2.py": "def test1(): pass", @@ -610,16 +636,16 @@ def pytest_ignore_collect(path, config): "root/demo-c/test_foo4.py": "def test1(): pass", } ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( ["*test_foo1.py*", "*test_foo3.py*", "*test_foo4.py*", "*3 passed*"] ) -def test_required_option_help(testdir): - testdir.makeconftest("assert 0") - x = testdir.mkdir("x") - x.join("conftest.py").write( +def test_required_option_help(pytester: Pytester) -> None: + pytester.makeconftest("assert 0") + x = pytester.mkdir("x") + x.joinpath("conftest.py").write_text( textwrap.dedent( """\ def pytest_addoption(parser): @@ -627,6 +653,6 @@ def pytest_addoption(parser): """ ) ) - result = testdir.runpytest("-h", x) + result = pytester.runpytest("-h", x) result.stdout.no_fnmatch_line("*argument --xyz is required*") assert "general:" in result.stdout.str() diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index f61f8586f9c..1aa0b5651d2 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -3,6 +3,7 @@ from typing import Optional import pytest +from _pytest.pytester import Pytester from _pytest.recwarn import WarningsRecorder @@ -12,8 +13,8 @@ def test_recwarn_stacklevel(recwarn: WarningsRecorder) -> None: assert warn.filename == __file__ -def test_recwarn_functional(testdir) -> None: - testdir.makepyfile( +def test_recwarn_functional(pytester: Pytester) -> None: + pytester.makepyfile( """ import warnings def test_method(recwarn): @@ -22,7 +23,7 @@ def test_method(recwarn): assert isinstance(warn.message, UserWarning) """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -328,9 +329,9 @@ class MyRuntimeWarning(RuntimeWarning): assert str(record[0].message) == "user" assert str(record[1].message) == "runtime" - def test_double_test(self, testdir) -> None: + def test_double_test(self, pytester: Pytester) -> None: """If a test is run again, the warning should still be raised""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest import warnings @@ -341,7 +342,7 @@ def test(run): warnings.warn("runtime", RuntimeWarning) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 passed in*"]) def test_match_regex(self) -> None: diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index bd6e7b968b0..21e3f79f274 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -18,14 +18,15 @@ from _pytest.pathlib import on_rm_rf_error from _pytest.pathlib import register_cleanup_lock_removal from _pytest.pathlib import rm_rf +from _pytest.pytester import Pytester from _pytest.tmpdir import get_user from _pytest.tmpdir import TempdirFactory from _pytest.tmpdir import TempPathFactory -def test_tmpdir_fixture(testdir): - p = testdir.copy_example("tmpdir/tmpdir_fixture.py") - results = testdir.runpytest(p) +def test_tmpdir_fixture(pytester: Pytester) -> None: + p = pytester.copy_example("tmpdir/tmpdir_fixture.py") + results = pytester.runpytest(p) results.stdout.fnmatch_lines(["*1 passed*"]) @@ -66,21 +67,21 @@ def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch): class TestConfigTmpdir: - def test_getbasetemp_custom_removes_old(self, testdir): - mytemp = testdir.tmpdir.join("xyz") - p = testdir.makepyfile( + def test_getbasetemp_custom_removes_old(self, pytester: Pytester) -> None: + mytemp = pytester.path.joinpath("xyz") + p = pytester.makepyfile( """ def test_1(tmpdir): pass """ ) - testdir.runpytest(p, "--basetemp=%s" % mytemp) - mytemp.check() - mytemp.ensure("hello") + pytester.runpytest(p, "--basetemp=%s" % mytemp) + assert mytemp.exists() + mytemp.joinpath("hello").touch() - testdir.runpytest(p, "--basetemp=%s" % mytemp) - mytemp.check() - assert not mytemp.join("hello").check() + pytester.runpytest(p, "--basetemp=%s" % mytemp) + assert mytemp.exists() + assert not mytemp.joinpath("hello").exists() testdata = [ @@ -96,9 +97,9 @@ def test_1(tmpdir): @pytest.mark.parametrize("basename, is_ok", testdata) -def test_mktemp(testdir, basename, is_ok): - mytemp = testdir.tmpdir.mkdir("mytemp") - p = testdir.makepyfile( +def test_mktemp(pytester: Pytester, basename: str, is_ok: bool) -> None: + mytemp = pytester.mkdir("mytemp") + p = pytester.makepyfile( """ def test_abs_path(tmpdir_factory): tmpdir_factory.mktemp('{}', numbered=False) @@ -107,54 +108,54 @@ def test_abs_path(tmpdir_factory): ) ) - result = testdir.runpytest(p, "--basetemp=%s" % mytemp) + result = pytester.runpytest(p, "--basetemp=%s" % mytemp) if is_ok: assert result.ret == 0 - assert mytemp.join(basename).check() + assert mytemp.joinpath(basename).exists() else: assert result.ret == 1 result.stdout.fnmatch_lines("*ValueError*") -def test_tmpdir_always_is_realpath(testdir): +def test_tmpdir_always_is_realpath(pytester: Pytester) -> None: # the reason why tmpdir should be a realpath is that # when you cd to it and do "os.getcwd()" you will anyway # get the realpath. Using the symlinked path can thus # easily result in path-inequality # XXX if that proves to be a problem, consider using # os.environ["PWD"] - realtemp = testdir.tmpdir.mkdir("myrealtemp") - linktemp = testdir.tmpdir.join("symlinktemp") + realtemp = pytester.mkdir("myrealtemp") + linktemp = pytester.path.joinpath("symlinktemp") attempt_symlink_to(linktemp, str(realtemp)) - p = testdir.makepyfile( + p = pytester.makepyfile( """ def test_1(tmpdir): import os assert os.path.realpath(str(tmpdir)) == str(tmpdir) """ ) - result = testdir.runpytest("-s", p, "--basetemp=%s/bt" % linktemp) + result = pytester.runpytest("-s", p, "--basetemp=%s/bt" % linktemp) assert not result.ret -def test_tmp_path_always_is_realpath(testdir, monkeypatch): +def test_tmp_path_always_is_realpath(pytester: Pytester, monkeypatch) -> None: # for reasoning see: test_tmpdir_always_is_realpath test-case - realtemp = testdir.tmpdir.mkdir("myrealtemp") - linktemp = testdir.tmpdir.join("symlinktemp") + realtemp = pytester.mkdir("myrealtemp") + linktemp = pytester.path.joinpath("symlinktemp") attempt_symlink_to(linktemp, str(realtemp)) monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(linktemp)) - testdir.makepyfile( + pytester.makepyfile( """ def test_1(tmp_path): assert tmp_path.resolve() == tmp_path """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) -def test_tmpdir_too_long_on_parametrization(testdir): - testdir.makepyfile( +def test_tmpdir_too_long_on_parametrization(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.mark.parametrize("arg", ["1"*1000]) @@ -162,12 +163,12 @@ def test_some(arg, tmpdir): tmpdir.ensure("hello") """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) -def test_tmpdir_factory(testdir): - testdir.makepyfile( +def test_tmpdir_factory(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture(scope='session') @@ -177,23 +178,23 @@ def test_some(session_dir): assert session_dir.isdir() """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) -def test_tmpdir_fallback_tox_env(testdir, monkeypatch): +def test_tmpdir_fallback_tox_env(pytester: Pytester, monkeypatch) -> None: """Test that tmpdir works even if environment variables required by getpass module are missing (#1010). """ monkeypatch.delenv("USER", raising=False) monkeypatch.delenv("USERNAME", raising=False) - testdir.makepyfile( + pytester.makepyfile( """ def test_some(tmpdir): assert tmpdir.isdir() """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -207,18 +208,18 @@ def break_getuser(monkeypatch): @pytest.mark.usefixtures("break_getuser") @pytest.mark.skipif(sys.platform.startswith("win"), reason="no os.getuid on windows") -def test_tmpdir_fallback_uid_not_found(testdir): +def test_tmpdir_fallback_uid_not_found(pytester: Pytester) -> None: """Test that tmpdir works even if the current process's user id does not correspond to a valid user. """ - testdir.makepyfile( + pytester.makepyfile( """ def test_some(tmpdir): assert tmpdir.isdir() """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -423,9 +424,9 @@ def test_tmpdir_equals_tmp_path(tmpdir, tmp_path): assert Path(tmpdir) == tmp_path -def test_basetemp_with_read_only_files(testdir): +def test_basetemp_with_read_only_files(pytester: Pytester) -> None: """Integration test for #5524""" - testdir.makepyfile( + pytester.makepyfile( """ import os import stat @@ -437,8 +438,8 @@ def test(tmp_path): os.chmod(str(fn), mode & ~stat.S_IREAD) """ ) - result = testdir.runpytest("--basetemp=tmp") + result = pytester.runpytest("--basetemp=tmp") assert result.ret == 0 # running a second time and ensure we don't crash - result = testdir.runpytest("--basetemp=tmp") + result = pytester.runpytest("--basetemp=tmp") assert result.ret == 0 From 3b677f79f4910db731d0cc1b204541d54717dc62 Mon Sep 17 00:00:00 2001 From: symonk Date: Mon, 16 Nov 2020 17:36:06 +0000 Subject: [PATCH 787/823] stop assigning nextline if its potentially not used --- src/_pytest/pytester.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 0e1dffb9d8a..e55b3ec1ace 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1819,7 +1819,6 @@ def _match_lines( raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__)) lines2 = self._getlines(lines2) lines1 = self.lines[:] - nextline = None extralines = [] __tracebackhide__ = True wnick = len(match_nickname) + 1 From b7ba76653d2c06c7d62fd475e515ce60119b65b9 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 19 Nov 2020 11:06:24 +0100 Subject: [PATCH 788/823] Prefix contextmanagers with module name in doc examples (#8044) * Prefix contextmanagers with module name in doc examples * Import pytest explicitly for doctests Co-authored-by: Bruno Oliveira --- src/_pytest/python_api.py | 13 +++++++------ src/_pytest/recwarn.py | 12 +++++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 9f4df8e7e9e..bae2076892b 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -597,7 +597,8 @@ def raises( Use ``pytest.raises`` as a context manager, which will capture the exception of the given type:: - >>> with raises(ZeroDivisionError): + >>> import pytest + >>> with pytest.raises(ZeroDivisionError): ... 1/0 If the code block does not raise the expected exception (``ZeroDivisionError`` in the example @@ -606,16 +607,16 @@ def raises( You can also use the keyword argument ``match`` to assert that the exception matches a text or regex:: - >>> with raises(ValueError, match='must be 0 or None'): + >>> with pytest.raises(ValueError, match='must be 0 or None'): ... raise ValueError("value must be 0 or None") - >>> with raises(ValueError, match=r'must be \d+$'): + >>> with pytest.raises(ValueError, match=r'must be \d+$'): ... raise ValueError("value must be 42") The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the details of the captured exception:: - >>> with raises(ValueError) as exc_info: + >>> with pytest.raises(ValueError) as exc_info: ... raise ValueError("value must be 42") >>> assert exc_info.type is ValueError >>> assert exc_info.value.args[0] == "value must be 42" @@ -629,7 +630,7 @@ def raises( not be executed. For example:: >>> value = 15 - >>> with raises(ValueError) as exc_info: + >>> with pytest.raises(ValueError) as exc_info: ... if value > 10: ... raise ValueError("value must be <= 10") ... assert exc_info.type is ValueError # this will not execute @@ -637,7 +638,7 @@ def raises( Instead, the following approach must be taken (note the difference in scope):: - >>> with raises(ValueError) as exc_info: + >>> with pytest.raises(ValueError) as exc_info: ... if value > 10: ... raise ValueError("value must be <= 10") ... diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 49f1e590296..6c04c2e165e 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -60,7 +60,8 @@ def deprecated_call( ... warnings.warn('use v3 of this api', DeprecationWarning) ... return 200 - >>> with deprecated_call(): + >>> import pytest + >>> with pytest.deprecated_call(): ... assert api_call_v2() == 200 It can also be used by passing a function and ``*args`` and ``**kwargs``, @@ -116,19 +117,20 @@ def warns( This function can be used as a context manager, or any of the other ways ``pytest.raises`` can be used:: - >>> with warns(RuntimeWarning): + >>> import pytest + >>> with pytest.warns(RuntimeWarning): ... warnings.warn("my warning", RuntimeWarning) In the context manager form you may use the keyword argument ``match`` to assert that the warning matches a text or regex:: - >>> with warns(UserWarning, match='must be 0 or None'): + >>> with pytest.warns(UserWarning, match='must be 0 or None'): ... warnings.warn("value must be 0 or None", UserWarning) - >>> with warns(UserWarning, match=r'must be \d+$'): + >>> with pytest.warns(UserWarning, match=r'must be \d+$'): ... warnings.warn("value must be 42", UserWarning) - >>> with warns(UserWarning, match=r'must be \d+$'): + >>> with pytest.warns(UserWarning, match=r'must be \d+$'): ... warnings.warn("this is not here", UserWarning) Traceback (most recent call last): ... From eda681af2b00a628fe2c747b5ec0ef90b4be5cbc Mon Sep 17 00:00:00 2001 From: Petter Strandmark Date: Thu, 19 Nov 2020 11:07:15 +0100 Subject: [PATCH 789/823] Call Python 3.8 doClassCleanups (#8033) --- AUTHORS | 1 + changelog/8032.improvement.rst | 1 + src/_pytest/unittest.py | 58 +++++++++--- testing/test_unittest.py | 160 +++++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 changelog/8032.improvement.rst diff --git a/AUTHORS b/AUTHORS index 8febe36ef0a..4b6786651ad 100644 --- a/AUTHORS +++ b/AUTHORS @@ -232,6 +232,7 @@ Pauli Virtanen Pavel Karateev Paweł Adamczak Pedro Algarvio +Petter Strandmark Philipp Loose Pieter Mulder Piotr Banaszkiewicz diff --git a/changelog/8032.improvement.rst b/changelog/8032.improvement.rst new file mode 100644 index 00000000000..1aca7206910 --- /dev/null +++ b/changelog/8032.improvement.rst @@ -0,0 +1 @@ +`doClassCleanups` (introduced in `unittest` in Python and 3.8) is now called. diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 21db0ec23f9..55f15efe4b7 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -99,26 +99,48 @@ def _inject_setup_teardown_fixtures(self, cls: type) -> None: """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding teardown functions (#517).""" class_fixture = _make_xunit_fixture( - cls, "setUpClass", "tearDownClass", scope="class", pass_self=False + cls, + "setUpClass", + "tearDownClass", + "doClassCleanups", + scope="class", + pass_self=False, ) if class_fixture: cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined] method_fixture = _make_xunit_fixture( - cls, "setup_method", "teardown_method", scope="function", pass_self=True + cls, + "setup_method", + "teardown_method", + None, + scope="function", + pass_self=True, ) if method_fixture: cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined] def _make_xunit_fixture( - obj: type, setup_name: str, teardown_name: str, scope: "_Scope", pass_self: bool + obj: type, + setup_name: str, + teardown_name: str, + cleanup_name: Optional[str], + scope: "_Scope", + pass_self: bool, ): setup = getattr(obj, setup_name, None) teardown = getattr(obj, teardown_name, None) if setup is None and teardown is None: return None + if cleanup_name: + cleanup = getattr(obj, cleanup_name, lambda *args: None) + else: + + def cleanup(*args): + pass + @pytest.fixture( scope=scope, autouse=True, @@ -130,16 +152,32 @@ def fixture(self, request: FixtureRequest) -> Generator[None, None, None]: reason = self.__unittest_skip_why__ pytest.skip(reason) if setup is not None: - if pass_self: - setup(self, request.function) - else: - setup() + try: + if pass_self: + setup(self, request.function) + else: + setup() + # unittest does not call the cleanup function for every BaseException, so we + # follow this here. + except Exception: + if pass_self: + cleanup(self) + else: + cleanup() + + raise yield - if teardown is not None: + try: + if teardown is not None: + if pass_self: + teardown(self, request.function) + else: + teardown() + finally: if pass_self: - teardown(self, request.function) + cleanup(self) else: - teardown() + cleanup() return fixture diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 2c8d03cb981..8b00cb826ac 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1260,3 +1260,163 @@ def test_plain_unittest_does_not_support_async(testdir): "*1 passed*", ] 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(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + @classmethod + def setUpClass(cls): + def cleanup(): + cls.values.append(1) + cls.addClassCleanup(cleanup) + def test_one(self): + pass + def test_two(self): + pass + def test_cleanup_called_exactly_once(): + assert MyTestCase.values == [1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 0 + 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(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + @classmethod + def setUpClass(cls): + def cleanup(): + cls.values.append(1) + cls.addClassCleanup(cleanup) + assert False + def test_one(self): + pass + def test_cleanup_called_exactly_once(): + assert MyTestCase.values == [1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 1 + 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(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + @classmethod + def setUpClass(cls): + def cleanup(): + cls.values.append(1) + cls.addClassCleanup(cleanup) + @classmethod + def tearDownClass(cls): + assert False + def test_one(self): + pass + def test_two(self): + pass + def test_cleanup_called_exactly_once(): + assert MyTestCase.values == [1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert passed == 3 + + +def test_do_cleanups_on_success(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + def setUp(self): + def cleanup(): + self.values.append(1) + self.addCleanup(cleanup) + def test_one(self): + pass + def test_two(self): + pass + def test_cleanup_called_the_right_number_of_times(): + assert MyTestCase.values == [1, 1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 0 + assert passed == 3 + + +def test_do_cleanups_on_setup_failure(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + def setUp(self): + def cleanup(): + self.values.append(1) + self.addCleanup(cleanup) + assert False + def test_one(self): + pass + def test_two(self): + pass + def test_cleanup_called_the_right_number_of_times(): + assert MyTestCase.values == [1, 1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 2 + assert passed == 1 + + +def test_do_cleanups_on_teardown_failure(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + def setUp(self): + def cleanup(): + self.values.append(1) + self.addCleanup(cleanup) + def tearDown(self): + assert False + def test_one(self): + pass + def test_two(self): + pass + def test_cleanup_called_the_right_number_of_times(): + assert MyTestCase.values == [1, 1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 2 + assert passed == 1 From ce825ed16cd17af35c27e584528e8b6909f8cd65 Mon Sep 17 00:00:00 2001 From: mickeypash Date: Wed, 18 Nov 2020 19:24:19 +0000 Subject: [PATCH 790/823] Add small section on migrating from nose to pytest The section currently features the nose2pytest tool with plans to expand on some of the common gotchas when performing such migrations. --- AUTHORS | 1 + doc/en/nose.rst | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/AUTHORS b/AUTHORS index 4b6786651ad..02bf0d9da3b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -200,6 +200,7 @@ Matthias Hafner Maxim Filipenko Maximilian Cosmo Sitter mbyt +Mickey Pashov Michael Aquilina Michael Birtwell Michael Droettboom diff --git a/doc/en/nose.rst b/doc/en/nose.rst index 1ac70af6c6b..44bbe440d09 100644 --- a/doc/en/nose.rst +++ b/doc/en/nose.rst @@ -68,4 +68,13 @@ Unsupported idioms / known issues fundamentally incompatible with pytest because they don't support fixtures properly since collection and test execution are separated. +Migrating from Nose to Pytest +------------------------------ + +`nose2pytest `_ is a Python script +and py.test plugin to help convert Nose-based tests into py.test-based tests. +Specifically, the script transforms nose.tools.assert_* function calls into +raw assert statements, while preserving format of original arguments +as much as possible. + .. _nose: https://nose.readthedocs.io/en/latest/ From c6ac618baf5f948a5038c2f93921479436a9e45c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 19 Nov 2020 13:54:40 +0100 Subject: [PATCH 791/823] Fix nose documentation Follow-up to #8048 which seems to have been merged without the suggested changes. --- doc/en/nose.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/nose.rst b/doc/en/nose.rst index 44bbe440d09..e16d764692c 100644 --- a/doc/en/nose.rst +++ b/doc/en/nose.rst @@ -68,11 +68,11 @@ Unsupported idioms / known issues fundamentally incompatible with pytest because they don't support fixtures properly since collection and test execution are separated. -Migrating from Nose to Pytest +Migrating from nose to pytest ------------------------------ `nose2pytest `_ is a Python script -and py.test plugin to help convert Nose-based tests into py.test-based tests. +and pytest plugin to help convert Nose-based tests into pytest-based tests. Specifically, the script transforms nose.tools.assert_* function calls into raw assert statements, while preserving format of original arguments as much as possible. From afd53ede6f3c950df3bfc9e81afdd45e09ef6d2b Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 19 Nov 2020 15:44:59 +0100 Subject: [PATCH 792/823] Link mentioned functions instead of using literals (#8045) --- doc/en/assert.rst | 8 ++++---- doc/en/skipping.rst | 4 ++-- doc/en/warnings.rst | 12 ++++++------ src/_pytest/recwarn.py | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 7e43b07fd75..b83e30e76db 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -74,7 +74,7 @@ Assertions about expected exceptions ------------------------------------------ In order to write assertions about raised exceptions, you can use -``pytest.raises`` as a context manager like this: +:func:`pytest.raises` as a context manager like this: .. code-block:: python @@ -123,7 +123,7 @@ The regexp parameter of the ``match`` method is matched with the ``re.search`` function, so in the above example ``match='123'`` would have worked as well. -There's an alternate form of the ``pytest.raises`` function where you pass +There's an alternate form of the :func:`pytest.raises` function where you pass a function that will be executed with the given ``*args`` and ``**kwargs`` and assert that the given exception is raised: @@ -144,8 +144,8 @@ specific way than just having any exception raised: def test_f(): f() -Using ``pytest.raises`` is likely to be better for cases where you are testing -exceptions your own code is deliberately raising, whereas using +Using :func:`pytest.raises` is likely to be better for cases where you are +testing exceptions your own code is deliberately raising, whereas using ``@pytest.mark.xfail`` with a check function is probably better for something like documenting unfixed bugs (where the test describes what "should" happen) or bugs in dependencies. diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index c463f3293bc..282820545c3 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -259,7 +259,7 @@ These two examples illustrate situations where you don't want to check for a con at the module level, which is when a condition would otherwise be evaluated for marks. This will make ``test_function`` ``XFAIL``. Note that no other code is executed after -the ``pytest.xfail`` call, differently from the marker. That's because it is implemented +the :func:`pytest.xfail` call, differently from the marker. That's because it is implemented internally by raising a known exception. **Reference**: :ref:`pytest.mark.xfail ref` @@ -358,7 +358,7 @@ By specifying on the commandline: pytest --runxfail you can force the running and reporting of an ``xfail`` marked test -as if it weren't marked at all. This also causes ``pytest.xfail`` to produce no effect. +as if it weren't marked at all. This also causes :func:`pytest.xfail` to produce no effect. Examples ~~~~~~~~ diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 7232b676d24..5bbbcacbea0 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -265,7 +265,7 @@ Asserting warnings with the warns function -You can check that code raises a particular warning using ``pytest.warns``, +You can check that code raises a particular warning using func:`pytest.warns`, which works in a similar manner to :ref:`raises `: .. code-block:: python @@ -293,7 +293,7 @@ argument ``match`` to assert that the exception matches a text or regex:: ... Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... -You can also call ``pytest.warns`` on a function or code string: +You can also call func:`pytest.warns` on a function or code string: .. code-block:: python @@ -328,10 +328,10 @@ Alternatively, you can examine raised warnings in detail using the Recording warnings ------------------ -You can record raised warnings either using ``pytest.warns`` or with +You can record raised warnings either using func:`pytest.warns` or with the ``recwarn`` fixture. -To record with ``pytest.warns`` without asserting anything about the warnings, +To record with func:`pytest.warns` without asserting anything about the warnings, pass ``None`` as the expected warning type: .. code-block:: python @@ -360,7 +360,7 @@ The ``recwarn`` fixture will record warnings for the whole function: assert w.filename assert w.lineno -Both ``recwarn`` and ``pytest.warns`` return the same interface for recorded +Both ``recwarn`` and func:`pytest.warns` return the same interface for recorded warnings: a WarningsRecorder instance. To view the recorded warnings, you can iterate over this instance, call ``len`` on it to get the number of recorded warnings, or index into it to get a particular recorded warning. @@ -387,7 +387,7 @@ are met. pytest.fail("Expected a warning!") If no warnings are issued when calling ``f``, then ``not record`` will -evaluate to ``True``. You can then call ``pytest.fail`` with a +evaluate to ``True``. You can then call :func:`pytest.fail` with a custom error message. .. _internal-warnings: diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 6c04c2e165e..4fe365cda4c 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -115,7 +115,7 @@ def warns( one for each warning raised. This function can be used as a context manager, or any of the other ways - ``pytest.raises`` can be used:: + :func:`pytest.raises` can be used:: >>> import pytest >>> with pytest.warns(RuntimeWarning): From 3e0bbd2f570e652375399eca8c7b57644612ae9d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 20 Nov 2020 17:54:03 +0200 Subject: [PATCH 793/823] testing: fix ResourceWarning in broken-pipe test --- testing/acceptance_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c937ce9dc1e..76ba75e36e3 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1288,3 +1288,6 @@ def test_no_brokenpipeerror_message(pytester: Pytester) -> None: ret = popen.wait() assert popen.stderr.read() == b"" assert ret == 1 + + # Cleanup. + popen.stderr.close() From 148e3c582afc6ae040d2ddcf09fade339a50dd1a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 20 Nov 2020 17:50:58 +0200 Subject: [PATCH 794/823] pytester: always close stdin pipe in pytester.run() If the user passed stdin=PIPE for some reason, they have no way to close it themselves since it is not exposed. --- src/_pytest/pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index e55b3ec1ace..dd13ef4ce35 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1349,7 +1349,7 @@ def run( stderr=f2, close_fds=(sys.platform != "win32"), ) - if isinstance(stdin, bytes): + if popen.stdin is not None: popen.stdin.close() def handle_timeout() -> None: From 52fef811c252fa65b555c8af7b857d7d064e46da Mon Sep 17 00:00:00 2001 From: Simon K Date: Sat, 21 Nov 2020 13:49:17 +0000 Subject: [PATCH 795/823] permit node to warn with any warning type, not just PytestWarning (#8052) Co-authored-by: Bruno Oliveira --- changelog/7615.improvement.rst | 1 + src/_pytest/nodes.py | 19 +++++++++++-------- testing/test_nodes.py | 26 +++++++++++++++++++++++--- 3 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 changelog/7615.improvement.rst diff --git a/changelog/7615.improvement.rst b/changelog/7615.improvement.rst new file mode 100644 index 00000000000..fcf9a1a9b42 --- /dev/null +++ b/changelog/7615.improvement.rst @@ -0,0 +1 @@ +:meth:`Node.warn <_pytest.nodes.Node.warn>` now permits any subclass of :class:`Warning`, not just :class:`PytestWarning `. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index dd58d5df9fd..27434fb6a67 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -34,7 +34,6 @@ if TYPE_CHECKING: # Imported here due to circular import. from _pytest.main import Session - from _pytest.warning_types import PytestWarning from _pytest._code.code import _TracebackStyle @@ -198,27 +197,31 @@ def ihook(self): def __repr__(self) -> str: return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) - def warn(self, warning: "PytestWarning") -> None: + def warn(self, warning: Warning) -> None: """Issue a warning for this Node. Warnings will be displayed after the test session, unless explicitly suppressed. :param Warning warning: - The warning instance to issue. Must be a subclass of PytestWarning. + The warning instance to issue. - :raises ValueError: If ``warning`` instance is not a subclass of PytestWarning. + :raises ValueError: If ``warning`` instance is not a subclass of Warning. Example usage: .. code-block:: python node.warn(PytestWarning("some message")) - """ - from _pytest.warning_types import PytestWarning + node.warn(UserWarning("some message")) - if not isinstance(warning, PytestWarning): + .. versionchanged:: 6.2 + Any subclass of :class:`Warning` is now accepted, rather than only + :class:`PytestWarning ` subclasses. + """ + # enforce type checks here to avoid getting a generic type error later otherwise. + if not isinstance(warning, Warning): raise ValueError( - "warning must be an instance of PytestWarning or subclass, got {!r}".format( + "warning must be an instance of Warning or subclass, got {!r}".format( warning ) ) diff --git a/testing/test_nodes.py b/testing/test_nodes.py index 627be930177..f3824c57090 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -1,10 +1,12 @@ from typing import List +from typing import Type import py import pytest from _pytest import nodes from _pytest.pytester import Pytester +from _pytest.warning_types import PytestWarning @pytest.mark.parametrize( @@ -35,15 +37,33 @@ def test_node_from_parent_disallowed_arguments() -> None: nodes.Node.from_parent(None, config=None) # type: ignore[arg-type] -def test_std_warn_not_pytestwarning(pytester: Pytester) -> None: +@pytest.mark.parametrize( + "warn_type, msg", [(DeprecationWarning, "deprecated"), (PytestWarning, "pytest")] +) +def test_node_warn_is_no_longer_only_pytest_warnings( + pytester: Pytester, warn_type: Type[Warning], msg: str +) -> None: + items = pytester.getitems( + """ + def test(): + pass + """ + ) + with pytest.warns(warn_type, match=msg): + items[0].warn(warn_type(msg)) + + +def test_node_warning_enforces_warning_types(pytester: Pytester) -> None: items = pytester.getitems( """ def test(): pass """ ) - with pytest.raises(ValueError, match=".*instance of PytestWarning.*"): - items[0].warn(UserWarning("some warning")) # type: ignore[arg-type] + with pytest.raises( + ValueError, match="warning must be an instance of Warning or subclass" + ): + items[0].warn(Exception("ok")) # type: ignore[arg-type] def test__check_initialpaths_for_relpath() -> None: From 31021ac8d569b5ccb89677c28aec6cdaae7272e0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 21 Nov 2020 11:05:54 -0300 Subject: [PATCH 796/823] Add links to some CHANGELOG entries While adding links to https://github.com/pytest-dev/pytest/pull/8052, noticed a few more missing. --- changelog/7425.feature.rst | 2 +- changelog/7527.improvement.rst | 2 +- changelog/7710.improvement.rst | 5 +++-- changelog/7911.bugfix.rst | 2 +- changelog/7913.bugfix.rst | 2 +- changelog/8023.improvement.rst | 2 +- changelog/8032.improvement.rst | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/changelog/7425.feature.rst b/changelog/7425.feature.rst index 55881d2074f..47e6f4dbd30 100644 --- a/changelog/7425.feature.rst +++ b/changelog/7425.feature.rst @@ -2,4 +2,4 @@ New :fixture:`pytester` fixture, which is identical to :fixture:`testdir` but it This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future. -Internally, the old :class:`Testdir` is now a thin wrapper around :class:`Pytester`, preserving the old interface. +Internally, the old :class:`Testdir <_pytest.pytester.Testdir>` is now a thin wrapper around :class:`Pytester <_pytest.pytester.Pytester>`, preserving the old interface. diff --git a/changelog/7527.improvement.rst b/changelog/7527.improvement.rst index 726acffa9f6..3a7e063fe6f 100644 --- a/changelog/7527.improvement.rst +++ b/changelog/7527.improvement.rst @@ -1 +1 @@ -When a comparison between `namedtuple` instances of the same type fails, pytest now shows the differing field names (possibly nested) instead of their indexes. +When a comparison between :func:`namedtuple ` instances of the same type fails, pytest now shows the differing field names (possibly nested) instead of their indexes. diff --git a/changelog/7710.improvement.rst b/changelog/7710.improvement.rst index 1bbaf7792ab..91b703ab60f 100644 --- a/changelog/7710.improvement.rst +++ b/changelog/7710.improvement.rst @@ -1,3 +1,4 @@ -Use strict equality comparison for nonnumeric types in ``approx`` instead of -raising ``TypeError``. +Use strict equality comparison for non-numeric types in :func:`pytest.approx` instead of +raising :class:`TypeError`. + This was the undocumented behavior before 3.7, but is now officially a supported feature. diff --git a/changelog/7911.bugfix.rst b/changelog/7911.bugfix.rst index 5b85b20b5bc..1ef783fbabb 100644 --- a/changelog/7911.bugfix.rst +++ b/changelog/7911.bugfix.rst @@ -1 +1 @@ -Directories created by `tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites. +Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites. diff --git a/changelog/7913.bugfix.rst b/changelog/7913.bugfix.rst index e8c4613bf4c..f42e7cb9cbd 100644 --- a/changelog/7913.bugfix.rst +++ b/changelog/7913.bugfix.rst @@ -1 +1 @@ -Fixed a crash or hang in ``pytester.spawn`` when the ``readline`` module is involved. +Fixed a crash or hang in :meth:`pytester.spawn <_pytest.pytester.Pytester.spawn>` when the :mod:`readline` module is involved. diff --git a/changelog/8023.improvement.rst b/changelog/8023.improvement.rst index 8d005ba0c70..c791dabc72d 100644 --- a/changelog/8023.improvement.rst +++ b/changelog/8023.improvement.rst @@ -1 +1 @@ -Added ``'node_modules'`` to default value for ``norecursedirs``. +Added ``'node_modules'`` to default value for :confval:`norecursedirs`. diff --git a/changelog/8032.improvement.rst b/changelog/8032.improvement.rst index 1aca7206910..76789ea5097 100644 --- a/changelog/8032.improvement.rst +++ b/changelog/8032.improvement.rst @@ -1 +1 @@ -`doClassCleanups` (introduced in `unittest` in Python and 3.8) is now called. +:meth:`doClassCleanups ` (introduced in :mod:`unittest` in Python and 3.8) is now called appropriately. From 0cef530d10850cfe0479a01c9f0cb7303ea163e3 Mon Sep 17 00:00:00 2001 From: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Date: Sat, 21 Nov 2020 19:45:20 +0100 Subject: [PATCH 797/823] Add str() support to LineMatcher (#8050) --- changelog/1265.improvement.rst | 1 + doc/en/reference.rst | 1 + src/_pytest/pytester.py | 12 ++++++++++-- testing/test_pytester.py | 5 +++++ 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 changelog/1265.improvement.rst diff --git a/changelog/1265.improvement.rst b/changelog/1265.improvement.rst new file mode 100644 index 00000000000..4e7d98c0a99 --- /dev/null +++ b/changelog/1265.improvement.rst @@ -0,0 +1 @@ +Added an ``__str__`` implementation to the :class:`~pytest.pytester.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 34be2c4540a..971ab1bef16 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -527,6 +527,7 @@ To use it, include in your topmost ``conftest.py`` file: .. autoclass:: LineMatcher() :members: + :special-members: __str__ .. autoclass:: HookRecorder() :members: diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index e55b3ec1ace..ab3ecbf59a3 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -512,7 +512,7 @@ def __init__( self.stdout = LineMatcher(outlines) """:class:`LineMatcher` of stdout. - Use e.g. :func:`stdout.str() ` to reconstruct stdout, or the commonly used + Use e.g. :func:`str(stdout) ` to reconstruct stdout, or the commonly used :func:`stdout.fnmatch_lines() ` method. """ self.stderr = LineMatcher(errlines) @@ -1707,6 +1707,14 @@ def __init__(self, lines: List[str]) -> None: self.lines = lines self._log_output: List[str] = [] + def __str__(self) -> str: + """Return the entire original text. + + .. versionadded:: 6.2 + You can use :meth:`str` in older versions. + """ + return "\n".join(self.lines) + def _getlines(self, lines2: Union[str, Sequence[str], Source]) -> Sequence[str]: if isinstance(lines2, str): lines2 = Source(lines2) @@ -1908,4 +1916,4 @@ def _fail(self, msg: str) -> None: def str(self) -> str: """Return the entire original text.""" - return "\n".join(self.lines) + return str(self) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 457a62dd396..f2e8dd5a36a 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -610,6 +610,11 @@ def test_linematcher_no_matching_after_match() -> None: assert str(e.value).splitlines() == ["fnmatch: '*'", " with: '1'"] +def test_linematcher_string_api() -> None: + lm = LineMatcher(["foo", "bar"]) + assert str(lm) == "foo\nbar" + + def test_pytester_addopts_before_testdir(request, monkeypatch) -> None: orig = os.environ.get("PYTEST_ADDOPTS", None) monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused") From d50df85e26b28e28ab5fb9a50df791068a051192 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 13 Nov 2020 23:47:19 +0200 Subject: [PATCH 798/823] Add unraisableexception and threadexception plugins --- changelog/5299.feature.rst | 2 + doc/en/reference.rst | 6 ++ doc/en/usage.rst | 32 +++++++ src/_pytest/config/__init__.py | 1 + src/_pytest/threadexception.py | 90 ++++++++++++++++++ src/_pytest/unraisableexception.py | 93 +++++++++++++++++++ src/_pytest/warning_types.py | 22 +++++ src/pytest/__init__.py | 4 + testing/test_threadexception.py | 137 ++++++++++++++++++++++++++++ testing/test_unraisableexception.py | 133 +++++++++++++++++++++++++++ 10 files changed, 520 insertions(+) create mode 100644 changelog/5299.feature.rst create mode 100644 src/_pytest/threadexception.py create mode 100644 src/_pytest/unraisableexception.py create mode 100644 testing/test_threadexception.py create mode 100644 testing/test_unraisableexception.py diff --git a/changelog/5299.feature.rst b/changelog/5299.feature.rst new file mode 100644 index 00000000000..7853e1833db --- /dev/null +++ b/changelog/5299.feature.rst @@ -0,0 +1,2 @@ +pytest now warns about unraisable exceptions and unhandled thread exceptions that occur in tests on Python>=3.8. +See :ref:`unraisable` for more information. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 34be2c4540a..b8a72b77eb6 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1090,6 +1090,12 @@ Custom warnings generated in some situations such as improper usage or deprecate .. autoclass:: pytest.PytestUnknownMarkWarning :show-inheritance: +.. autoclass:: pytest.PytestUnraisableExceptionWarning + :show-inheritance: + +.. autoclass:: pytest.PytestUnhandledThreadExceptionWarning + :show-inheritance: + Consult the :ref:`internal-warnings` section in the documentation for more information. diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 3c03db4540f..fbd3333dabc 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -470,6 +470,38 @@ seconds to finish (not available on Windows). the command-line using ``-o faulthandler_timeout=X``. +.. _unraisable: + +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. + +Unhandled thread exceptions are exceptions raised in a :class:`~threading.Thread` +but not handled, causing the thread to terminate uncleanly. + +Both types of exceptions are normally considered bugs, but may go unnoticed +because they don't cause the program itself to crash. Pytest detects these +conditions and issues a warning that is visible in the test run summary. + +The plugins are automatically enabled for pytest runs, unless the +``-p no:unraisableexception`` (for unraisable exceptions) and +``-p no:threadexception`` (for thread exceptions) options are given on the +command-line. + +The warnings may be silenced selectivly using the :ref:`pytest.mark.filterwarnings ref` +mark. The warning categories are :class:`pytest.PytestUnraisableExceptionWarning` and +:class:`pytest.PytestUnhandledThreadExceptionWarning`. + + Creating JUnitXML format files ---------------------------------------------------- diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6c1d9c69a50..10201c47f61 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -251,6 +251,7 @@ def directory_arg(path: str, optname: str) -> str: "warnings", "logging", "reports", + *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), "faulthandler", ) diff --git a/src/_pytest/threadexception.py b/src/_pytest/threadexception.py new file mode 100644 index 00000000000..1c1f62fdb73 --- /dev/null +++ b/src/_pytest/threadexception.py @@ -0,0 +1,90 @@ +import threading +import traceback +import warnings +from types import TracebackType +from typing import Any +from typing import Callable +from typing import Generator +from typing import Optional +from typing import Type + +import pytest + + +# Copied from cpython/Lib/test/support/threading_helper.py, with modifications. +class catch_threading_exception: + """Context manager catching threading.Thread exception using + threading.excepthook. + + Storing exc_value using a custom hook can create a reference cycle. The + reference cycle is broken explicitly when the context manager exits. + + Storing thread using a custom hook can resurrect it if it is set to an + object which is being finalized. Exiting the context manager clears the + stored object. + + Usage: + with threading_helper.catch_threading_exception() as cm: + # code spawning a thread which raises an exception + ... + # check the thread exception: use cm.args + ... + # cm.args attribute no longer exists at this point + # (to break a reference cycle) + """ + + def __init__(self) -> None: + # See https://github.com/python/typeshed/issues/4767 regarding the underscore. + self.args: Optional["threading._ExceptHookArgs"] = None + self._old_hook: Optional[Callable[["threading._ExceptHookArgs"], Any]] = None + + def _hook(self, args: "threading._ExceptHookArgs") -> None: + self.args = args + + def __enter__(self) -> "catch_threading_exception": + self._old_hook = threading.excepthook + threading.excepthook = self._hook + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + assert self._old_hook is not None + threading.excepthook = self._old_hook + self._old_hook = None + del self.args + + +def thread_exception_runtest_hook() -> Generator[None, None, None]: + with catch_threading_exception() as cm: + yield + if cm.args: + if cm.args.thread is not None: + thread_name = cm.args.thread.name + else: + 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, + ) + ) + warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg)) + + +@pytest.hookimpl(hookwrapper=True, trylast=True) +def pytest_runtest_setup() -> Generator[None, None, None]: + yield from thread_exception_runtest_hook() + + +@pytest.hookimpl(hookwrapper=True, tryfirst=True) +def pytest_runtest_call() -> Generator[None, None, None]: + yield from thread_exception_runtest_hook() + + +@pytest.hookimpl(hookwrapper=True, tryfirst=True) +def pytest_runtest_teardown() -> Generator[None, None, None]: + yield from thread_exception_runtest_hook() diff --git a/src/_pytest/unraisableexception.py b/src/_pytest/unraisableexception.py new file mode 100644 index 00000000000..fcb5d8237c1 --- /dev/null +++ b/src/_pytest/unraisableexception.py @@ -0,0 +1,93 @@ +import sys +import traceback +import warnings +from types import TracebackType +from typing import Any +from typing import Callable +from typing import Generator +from typing import Optional +from typing import Type + +import pytest + + +# Copied from cpython/Lib/test/support/__init__.py, with modifications. +class catch_unraisable_exception: + """Context manager catching unraisable exception using sys.unraisablehook. + + Storing the exception value (cm.unraisable.exc_value) creates a reference + cycle. The reference cycle is broken explicitly when the context manager + exits. + + Storing the object (cm.unraisable.object) can resurrect it if it is set to + an object which is being finalized. Exiting the context manager clears the + stored object. + + Usage: + with catch_unraisable_exception() as cm: + # code creating an "unraisable exception" + ... + # check the unraisable exception: use cm.unraisable + ... + # cm.unraisable attribute no longer exists at this point + # (to break a reference cycle) + """ + + def __init__(self) -> None: + self.unraisable: Optional["sys.UnraisableHookArgs"] = None + self._old_hook: Optional[Callable[["sys.UnraisableHookArgs"], Any]] = None + + def _hook(self, unraisable: "sys.UnraisableHookArgs") -> None: + # Storing unraisable.object can resurrect an object which is being + # finalized. Storing unraisable.exc_value creates a reference cycle. + self.unraisable = unraisable + + def __enter__(self) -> "catch_unraisable_exception": + self._old_hook = sys.unraisablehook + sys.unraisablehook = self._hook + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + assert self._old_hook is not None + sys.unraisablehook = self._old_hook + self._old_hook = None + del self.unraisable + + +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, + ) + ) + warnings.warn(pytest.PytestUnraisableExceptionWarning(msg)) + + +@pytest.hookimpl(hookwrapper=True, tryfirst=True) +def pytest_runtest_setup() -> Generator[None, None, None]: + yield from unraisable_exception_runtest_hook() + + +@pytest.hookimpl(hookwrapper=True, tryfirst=True) +def pytest_runtest_call() -> Generator[None, None, None]: + yield from unraisable_exception_runtest_hook() + + +@pytest.hookimpl(hookwrapper=True, tryfirst=True) +def pytest_runtest_teardown() -> Generator[None, None, None]: + yield from unraisable_exception_runtest_hook() diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 2fd4d4f6e8f..2eadd9fe4db 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -90,6 +90,28 @@ class PytestUnknownMarkWarning(PytestWarning): __module__ = "pytest" +@final +class PytestUnraisableExceptionWarning(PytestWarning): + """An unraisable exception was reported. + + Unraisable exceptions are exceptions raised in :meth:`__del__ ` + implementations and similar situations when the exception cannot be raised + as normal. + """ + + __module__ = "pytest" + + +@final +class PytestUnhandledThreadExceptionWarning(PytestWarning): + """An unhandled exception occurred in a :class:`~threading.Thread`. + + Such exceptions don't propagate normally. + """ + + __module__ = "pytest" + + _W = TypeVar("_W", bound=PytestWarning) diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index d7a5b22997f..6efdfcf9dc8 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -44,7 +44,9 @@ from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestExperimentalApiWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning +from _pytest.warning_types import PytestUnhandledThreadExceptionWarning from _pytest.warning_types import PytestUnknownMarkWarning +from _pytest.warning_types import PytestUnraisableExceptionWarning from _pytest.warning_types import PytestWarning set_trace = __pytestPDB.set_trace @@ -85,7 +87,9 @@ "PytestDeprecationWarning", "PytestExperimentalApiWarning", "PytestUnhandledCoroutineWarning", + "PytestUnhandledThreadExceptionWarning", "PytestUnknownMarkWarning", + "PytestUnraisableExceptionWarning", "PytestWarning", "raises", "register_assert_rewrite", diff --git a/testing/test_threadexception.py b/testing/test_threadexception.py new file mode 100644 index 00000000000..399692bc963 --- /dev/null +++ b/testing/test_threadexception.py @@ -0,0 +1,137 @@ +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") +def test_unhandled_thread_exception(pytester: Pytester) -> None: + pytester.makepyfile( + test_it=""" + import threading + + def test_it(): + def oops(): + raise ValueError("Oops") + + t = threading.Thread(target=oops, name="MyThread") + t.start() + t.join() + + def test_2(): pass + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + assert result.parseoutcomes() == {"passed": 2, "warnings": 1} + result.stdout.fnmatch_lines( + [ + "*= warnings summary =*", + "test_it.py::test_it", + " * PytestUnhandledThreadExceptionWarning: Exception in thread MyThread", + " ", + " Traceback (most recent call last):", + " ValueError: Oops", + " ", + " warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))", + ] + ) + + +@pytest.mark.filterwarnings("default") +def test_unhandled_thread_exception_in_setup(pytester: Pytester) -> None: + pytester.makepyfile( + test_it=""" + import threading + import pytest + + @pytest.fixture + def threadexc(): + def oops(): + raise ValueError("Oops") + t = threading.Thread(target=oops, name="MyThread") + t.start() + t.join() + + def test_it(threadexc): pass + def test_2(): pass + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + assert result.parseoutcomes() == {"passed": 2, "warnings": 1} + result.stdout.fnmatch_lines( + [ + "*= warnings summary =*", + "test_it.py::test_it", + " * PytestUnhandledThreadExceptionWarning: Exception in thread MyThread", + " ", + " Traceback (most recent call last):", + " ValueError: Oops", + " ", + " warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))", + ] + ) + + +@pytest.mark.filterwarnings("default") +def test_unhandled_thread_exception_in_teardown(pytester: Pytester) -> None: + pytester.makepyfile( + test_it=""" + import threading + import pytest + + @pytest.fixture + def threadexc(): + def oops(): + raise ValueError("Oops") + yield + t = threading.Thread(target=oops, name="MyThread") + t.start() + t.join() + + def test_it(threadexc): pass + def test_2(): pass + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + assert result.parseoutcomes() == {"passed": 2, "warnings": 1} + result.stdout.fnmatch_lines( + [ + "*= warnings summary =*", + "test_it.py::test_it", + " * PytestUnhandledThreadExceptionWarning: Exception in thread MyThread", + " ", + " Traceback (most recent call last):", + " ValueError: Oops", + " ", + " warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))", + ] + ) + + +@pytest.mark.filterwarnings("error::pytest.PytestUnhandledThreadExceptionWarning") +def test_unhandled_thread_exception_warning_error(pytester: Pytester) -> None: + pytester.makepyfile( + test_it=""" + import threading + import pytest + + def test_it(): + def oops(): + raise ValueError("Oops") + t = threading.Thread(target=oops, name="MyThread") + t.start() + t.join() + + def test_2(): pass + """ + ) + result = pytester.runpytest() + assert result.ret == pytest.ExitCode.TESTS_FAILED + assert result.parseoutcomes() == {"passed": 1, "failed": 1} diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py new file mode 100644 index 00000000000..32f89033409 --- /dev/null +++ b/testing/test_unraisableexception.py @@ -0,0 +1,133 @@ +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") +def test_unraisable(pytester: Pytester) -> None: + pytester.makepyfile( + test_it=""" + class BrokenDel: + def __del__(self): + raise ValueError("del is broken") + + def test_it(): + obj = BrokenDel() + del obj + + def test_2(): pass + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + assert result.parseoutcomes() == {"passed": 2, "warnings": 1} + result.stdout.fnmatch_lines( + [ + "*= warnings summary =*", + "test_it.py::test_it", + " * PytestUnraisableExceptionWarning: Exception ignored in: ", + " ", + " Traceback (most recent call last):", + " ValueError: del is broken", + " ", + " warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))", + ] + ) + + +@pytest.mark.filterwarnings("default") +def test_unraisable_in_setup(pytester: Pytester) -> None: + pytester.makepyfile( + test_it=""" + import pytest + + class BrokenDel: + def __del__(self): + raise ValueError("del is broken") + + @pytest.fixture + def broken_del(): + obj = BrokenDel() + del obj + + def test_it(broken_del): pass + def test_2(): pass + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + assert result.parseoutcomes() == {"passed": 2, "warnings": 1} + result.stdout.fnmatch_lines( + [ + "*= warnings summary =*", + "test_it.py::test_it", + " * PytestUnraisableExceptionWarning: Exception ignored in: ", + " ", + " Traceback (most recent call last):", + " ValueError: del is broken", + " ", + " warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))", + ] + ) + + +@pytest.mark.filterwarnings("default") +def test_unraisable_in_teardown(pytester: Pytester) -> None: + pytester.makepyfile( + test_it=""" + import pytest + + class BrokenDel: + def __del__(self): + raise ValueError("del is broken") + + @pytest.fixture + def broken_del(): + yield + obj = BrokenDel() + del obj + + def test_it(broken_del): pass + def test_2(): pass + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + assert result.parseoutcomes() == {"passed": 2, "warnings": 1} + result.stdout.fnmatch_lines( + [ + "*= warnings summary =*", + "test_it.py::test_it", + " * PytestUnraisableExceptionWarning: Exception ignored in: ", + " ", + " Traceback (most recent call last):", + " ValueError: del is broken", + " ", + " warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))", + ] + ) + + +@pytest.mark.filterwarnings("error::pytest.PytestUnraisableExceptionWarning") +def test_unraisable_warning_error(pytester: Pytester) -> None: + pytester.makepyfile( + test_it=""" + class BrokenDel: + def __del__(self) -> None: + raise ValueError("del is broken") + + def test_it() -> None: + obj = BrokenDel() + del obj + + def test_2(): pass + """ + ) + result = pytester.runpytest() + assert result.ret == pytest.ExitCode.TESTS_FAILED + assert result.parseoutcomes() == {"passed": 1, "failed": 1} From b3108723007e32fcb650a2ade8c697f12fd3f716 Mon Sep 17 00:00:00 2001 From: Simon K Date: Mon, 23 Nov 2020 20:45:12 +0000 Subject: [PATCH 799/823] fix mock_timing fixture name (typo) in timing.py --- src/_pytest/timing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/timing.py b/src/_pytest/timing.py index 62442de7528..925163a5858 100644 --- a/src/_pytest/timing.py +++ b/src/_pytest/timing.py @@ -3,7 +3,7 @@ We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect pytest runtime information (issue #185). -Fixture "mock_timinig" also interacts with this module for pytest's own tests. +Fixture "mock_timing" also interacts with this module for pytest's own tests. """ from time import perf_counter from time import sleep From d27806295a3a7249839491e82d0b87bdfbf37383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Gmach?= Date: Tue, 24 Nov 2020 22:27:34 +0100 Subject: [PATCH 800/823] fix typo (#8069) --- src/_pytest/_argcomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/_argcomplete.py b/src/_pytest/_argcomplete.py index 63deb667d0f..41d9d9407c7 100644 --- a/src/_pytest/_argcomplete.py +++ b/src/_pytest/_argcomplete.py @@ -26,7 +26,7 @@ uses a python program to determine startup script generated by pip. You can speed up completion somewhat by changing this script to include # PYTHON_ARGCOMPLETE_OK -so the the python-argcomplete-check-easy-install-script does not +so the python-argcomplete-check-easy-install-script does not need to be called to find the entry point of the code and see if that is marked with PYTHON_ARGCOMPLETE_OK. From 775ba63c67f85d8dd50326ab9e3e3710483480e0 Mon Sep 17 00:00:00 2001 From: Dominic Mortlock Date: Wed, 25 Nov 2020 04:42:47 -0800 Subject: [PATCH 801/823] Refactor acceptance_test to use pytester (#8070) --- AUTHORS | 1 + testing/acceptance_test.py | 569 +++++++++++++++++++------------------ 2 files changed, 299 insertions(+), 271 deletions(-) diff --git a/AUTHORS b/AUTHORS index 02bf0d9da3b..85d5b90de3d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -89,6 +89,7 @@ Dhiren Serai Diego Russo Dmitry Dygalo Dmitry Pribysh +Dominic Mortlock Duncan Betts Edison Gustavo Muenz Edoardo Batini diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c937ce9dc1e..fd516a23a2e 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -12,7 +12,7 @@ from _pytest.pytester import Pytester -def prepend_pythonpath(*dirs): +def prepend_pythonpath(*dirs) -> str: cur = os.getenv("PYTHONPATH") if cur: dirs += (cur,) @@ -20,60 +20,60 @@ def prepend_pythonpath(*dirs): class TestGeneralUsage: - def test_config_error(self, testdir): - testdir.copy_example("conftest_usageerror/conftest.py") - result = testdir.runpytest(testdir.tmpdir) + def test_config_error(self, pytester: Pytester) -> None: + pytester.copy_example("conftest_usageerror/conftest.py") + result = pytester.runpytest(pytester.path) assert result.ret == ExitCode.USAGE_ERROR result.stderr.fnmatch_lines(["*ERROR: hello"]) result.stdout.fnmatch_lines(["*pytest_unconfigure_called"]) - def test_root_conftest_syntax_error(self, testdir): - testdir.makepyfile(conftest="raise SyntaxError\n") - result = testdir.runpytest() + def test_root_conftest_syntax_error(self, pytester: Pytester) -> None: + pytester.makepyfile(conftest="raise SyntaxError\n") + result = pytester.runpytest() result.stderr.fnmatch_lines(["*raise SyntaxError*"]) assert result.ret != 0 - def test_early_hook_error_issue38_1(self, testdir): - testdir.makeconftest( + def test_early_hook_error_issue38_1(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_sessionstart(): 0 / 0 """ ) - result = testdir.runpytest(testdir.tmpdir) + result = pytester.runpytest(pytester.path) assert result.ret != 0 # tracestyle is native by default for hook failures result.stdout.fnmatch_lines( ["*INTERNALERROR*File*conftest.py*line 2*", "*0 / 0*"] ) - result = testdir.runpytest(testdir.tmpdir, "--fulltrace") + result = pytester.runpytest(pytester.path, "--fulltrace") assert result.ret != 0 # tracestyle is native by default for hook failures result.stdout.fnmatch_lines( ["*INTERNALERROR*def pytest_sessionstart():*", "*INTERNALERROR*0 / 0*"] ) - def test_early_hook_configure_error_issue38(self, testdir): - testdir.makeconftest( + def test_early_hook_configure_error_issue38(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_configure(): 0 / 0 """ ) - result = testdir.runpytest(testdir.tmpdir) + result = pytester.runpytest(pytester.path) assert result.ret != 0 # here we get it on stderr result.stderr.fnmatch_lines( ["*INTERNALERROR*File*conftest.py*line 2*", "*0 / 0*"] ) - def test_file_not_found(self, testdir): - result = testdir.runpytest("asd") + def test_file_not_found(self, pytester: Pytester) -> None: + result = pytester.runpytest("asd") assert result.ret != 0 result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"]) - def test_file_not_found_unconfigure_issue143(self, testdir): - testdir.makeconftest( + def test_file_not_found_unconfigure_issue143(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_configure(): print("---configure") @@ -81,36 +81,38 @@ def pytest_unconfigure(): print("---unconfigure") """ ) - result = testdir.runpytest("-s", "asd") + result = pytester.runpytest("-s", "asd") assert result.ret == ExitCode.USAGE_ERROR result.stderr.fnmatch_lines(["ERROR: file or directory not found: asd"]) result.stdout.fnmatch_lines(["*---configure", "*---unconfigure"]) - def test_config_preparse_plugin_option(self, testdir): - testdir.makepyfile( + def test_config_preparse_plugin_option(self, pytester: Pytester) -> None: + pytester.makepyfile( pytest_xyz=""" def pytest_addoption(parser): parser.addoption("--xyz", dest="xyz", action="store") """ ) - testdir.makepyfile( + pytester.makepyfile( test_one=""" def test_option(pytestconfig): assert pytestconfig.option.xyz == "123" """ ) - result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123", syspathinsert=True) + result = pytester.runpytest("-p", "pytest_xyz", "--xyz=123", syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) @pytest.mark.parametrize("load_cov_early", [True, False]) - def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early): + def test_early_load_setuptools_name( + self, pytester: Pytester, monkeypatch, load_cov_early + ) -> None: monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") - testdir.makepyfile(mytestplugin1_module="") - testdir.makepyfile(mytestplugin2_module="") - testdir.makepyfile(mycov_module="") - testdir.syspathinsert() + pytester.makepyfile(mytestplugin1_module="") + pytester.makepyfile(mytestplugin2_module="") + pytester.makepyfile(mycov_module="") + pytester.syspathinsert() loaded = [] @@ -141,35 +143,35 @@ def my_dists(): monkeypatch.setattr(importlib_metadata, "distributions", my_dists) params = ("-p", "mycov") if load_cov_early else () - testdir.runpytest_inprocess(*params) + pytester.runpytest_inprocess(*params) if load_cov_early: assert loaded == ["mycov", "myplugin1", "myplugin2"] else: assert loaded == ["myplugin1", "myplugin2", "mycov"] @pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"]) - def test_assertion_rewrite(self, testdir, import_mode): - p = testdir.makepyfile( + def test_assertion_rewrite(self, pytester: Pytester, import_mode) -> None: + p = pytester.makepyfile( """ def test_this(): x = 0 assert x """ ) - result = testdir.runpytest(p, f"--import-mode={import_mode}") + result = pytester.runpytest(p, f"--import-mode={import_mode}") result.stdout.fnmatch_lines(["> assert x", "E assert 0"]) assert result.ret == 1 - def test_nested_import_error(self, testdir): - p = testdir.makepyfile( + def test_nested_import_error(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import import_fails def test_this(): assert import_fails.a == 1 """ ) - testdir.makepyfile(import_fails="import does_not_work") - result = testdir.runpytest(p) + pytester.makepyfile(import_fails="import does_not_work") + result = pytester.runpytest(p) result.stdout.fnmatch_lines( [ "ImportError while importing test module*", @@ -178,10 +180,10 @@ def test_this(): ) assert result.ret == 2 - def test_not_collectable_arguments(self, testdir): - p1 = testdir.makepyfile("") - p2 = testdir.makefile(".pyc", "123") - result = testdir.runpytest(p1, p2) + def test_not_collectable_arguments(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile("") + p2 = pytester.makefile(".pyc", "123") + result = pytester.runpytest(p1, p2) assert result.ret == ExitCode.USAGE_ERROR result.stderr.fnmatch_lines( [ @@ -192,24 +194,26 @@ def test_not_collectable_arguments(self, testdir): ) @pytest.mark.filterwarnings("default") - def test_better_reporting_on_conftest_load_failure(self, testdir): + def test_better_reporting_on_conftest_load_failure( + self, pytester: Pytester + ) -> None: """Show a user-friendly traceback on conftest import failures (#486, #3332)""" - testdir.makepyfile("") - conftest = testdir.makeconftest( + pytester.makepyfile("") + conftest = pytester.makeconftest( """ def foo(): import qwerty foo() """ ) - result = testdir.runpytest("--help") + result = pytester.runpytest("--help") result.stdout.fnmatch_lines( """ *--version* *warning*conftest.py* """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.stdout.lines == [] assert result.stderr.lines == [ f"ImportError while loading conftest '{conftest}'.", @@ -220,77 +224,78 @@ def foo(): "E ModuleNotFoundError: No module named 'qwerty'", ] - def test_early_skip(self, testdir): - testdir.mkdir("xyz") - testdir.makeconftest( + def test_early_skip(self, pytester: Pytester) -> None: + pytester.mkdir("xyz") + pytester.makeconftest( """ import pytest def pytest_collect_file(): pytest.skip("early") """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines(["*1 skip*"]) - def test_issue88_initial_file_multinodes(self, testdir): - testdir.copy_example("issue88_initial_file_multinodes") - p = testdir.makepyfile("def test_hello(): pass") - result = testdir.runpytest(p, "--collect-only") + def test_issue88_initial_file_multinodes(self, pytester: Pytester) -> None: + pytester.copy_example("issue88_initial_file_multinodes") + p = pytester.makepyfile("def test_hello(): pass") + result = pytester.runpytest(p, "--collect-only") result.stdout.fnmatch_lines(["*MyFile*test_issue88*", "*Module*test_issue88*"]) - def test_issue93_initialnode_importing_capturing(self, testdir): - testdir.makeconftest( + def test_issue93_initialnode_importing_capturing(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import sys print("should not be seen") sys.stderr.write("stder42\\n") """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.no_fnmatch_line("*should not be seen*") assert "stderr42" not in result.stderr.str() - def test_conftest_printing_shows_if_error(self, testdir): - testdir.makeconftest( + def test_conftest_printing_shows_if_error(self, pytester: Pytester) -> None: + pytester.makeconftest( """ print("should be seen") assert 0 """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret != 0 assert "should be seen" in result.stdout.str() - def test_issue109_sibling_conftests_not_loaded(self, testdir): - sub1 = testdir.mkdir("sub1") - sub2 = testdir.mkdir("sub2") - sub1.join("conftest.py").write("assert 0") - result = testdir.runpytest(sub2) + def test_issue109_sibling_conftests_not_loaded(self, pytester: Pytester) -> None: + sub1 = pytester.mkdir("sub1") + sub2 = pytester.mkdir("sub2") + sub1.joinpath("conftest.py").write_text("assert 0") + result = pytester.runpytest(sub2) assert result.ret == ExitCode.NO_TESTS_COLLECTED - sub2.ensure("__init__.py") - p = sub2.ensure("test_hello.py") - result = testdir.runpytest(p) + sub2.joinpath("__init__.py").touch() + p = sub2.joinpath("test_hello.py") + p.touch() + result = pytester.runpytest(p) assert result.ret == ExitCode.NO_TESTS_COLLECTED - result = testdir.runpytest(sub1) + result = pytester.runpytest(sub1) assert result.ret == ExitCode.USAGE_ERROR - def test_directory_skipped(self, testdir): - testdir.makeconftest( + def test_directory_skipped(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest def pytest_ignore_collect(): pytest.skip("intentional") """ ) - testdir.makepyfile("def test_hello(): pass") - result = testdir.runpytest() + pytester.makepyfile("def test_hello(): pass") + result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stdout.fnmatch_lines(["*1 skipped*"]) - def test_multiple_items_per_collector_byid(self, testdir): - c = testdir.makeconftest( + def test_multiple_items_per_collector_byid(self, pytester: Pytester) -> None: + c = pytester.makeconftest( """ import pytest class MyItem(pytest.Item): @@ -304,12 +309,12 @@ def pytest_collect_file(path, parent): return MyCollector.from_parent(fspath=path, parent=parent) """ ) - result = testdir.runpytest(c.basename + "::" + "xyz") + result = pytester.runpytest(c.name + "::" + "xyz") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 pass*"]) - def test_skip_on_generated_funcarg_id(self, testdir): - testdir.makeconftest( + def test_skip_on_generated_funcarg_id(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest def pytest_generate_tests(metafunc): @@ -321,13 +326,13 @@ def pytest_runtest_setup(item): assert 0 """ ) - p = testdir.makepyfile("""def test_func(x): pass""") - res = testdir.runpytest(p) + p = pytester.makepyfile("""def test_func(x): pass""") + res = pytester.runpytest(p) assert res.ret == 0 res.stdout.fnmatch_lines(["*1 skipped*"]) - def test_direct_addressing_selects(self, testdir): - p = testdir.makepyfile( + def test_direct_addressing_selects(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def pytest_generate_tests(metafunc): metafunc.parametrize('i', [1, 2], ids=["1", "2"]) @@ -335,56 +340,58 @@ def test_func(i): pass """ ) - res = testdir.runpytest(p.basename + "::" + "test_func[1]") + res = pytester.runpytest(p.name + "::" + "test_func[1]") assert res.ret == 0 res.stdout.fnmatch_lines(["*1 passed*"]) - def test_direct_addressing_notfound(self, testdir): - p = testdir.makepyfile( + def test_direct_addressing_notfound(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def test_func(): pass """ ) - res = testdir.runpytest(p.basename + "::" + "test_notfound") + res = pytester.runpytest(p.name + "::" + "test_notfound") assert res.ret res.stderr.fnmatch_lines(["*ERROR*not found*"]) - def test_docstring_on_hookspec(self): + def test_docstring_on_hookspec(self) -> None: from _pytest import hookspec for name, value in vars(hookspec).items(): if name.startswith("pytest_"): assert value.__doc__, "no docstring for %s" % name - def test_initialization_error_issue49(self, testdir): - testdir.makeconftest( + def test_initialization_error_issue49(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_configure(): x """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 3 # internal error result.stderr.fnmatch_lines(["INTERNAL*pytest_configure*", "INTERNAL*x*"]) assert "sessionstarttime" not in result.stderr.str() @pytest.mark.parametrize("lookfor", ["test_fun.py::test_a"]) - def test_issue134_report_error_when_collecting_member(self, testdir, lookfor): - testdir.makepyfile( + def test_issue134_report_error_when_collecting_member( + self, pytester: Pytester, lookfor + ) -> None: + pytester.makepyfile( test_fun=""" def test_a(): pass def""" ) - result = testdir.runpytest(lookfor) + result = pytester.runpytest(lookfor) result.stdout.fnmatch_lines(["*SyntaxError*"]) if "::" in lookfor: result.stderr.fnmatch_lines(["*ERROR*"]) assert result.ret == 4 # usage error only if item not found - def test_report_all_failed_collections_initargs(self, testdir): - testdir.makeconftest( + def test_report_all_failed_collections_initargs(self, pytester: Pytester) -> None: + pytester.makeconftest( """ from _pytest.config import ExitCode @@ -393,21 +400,23 @@ def pytest_sessionfinish(exitstatus): print("pytest_sessionfinish_called") """ ) - testdir.makepyfile(test_a="def", test_b="def") - result = testdir.runpytest("test_a.py::a", "test_b.py::b") + pytester.makepyfile(test_a="def", test_b="def") + result = pytester.runpytest("test_a.py::a", "test_b.py::b") result.stderr.fnmatch_lines(["*ERROR*test_a.py::a*", "*ERROR*test_b.py::b*"]) result.stdout.fnmatch_lines(["pytest_sessionfinish_called"]) assert result.ret == ExitCode.USAGE_ERROR - def test_namespace_import_doesnt_confuse_import_hook(self, testdir): + def test_namespace_import_doesnt_confuse_import_hook( + self, pytester: Pytester + ) -> None: """Ref #383. Python 3.3's namespace package messed with our import hooks. Importing a module that didn't exist, even if the ImportError was gracefully handled, would make our test crash. """ - testdir.mkdir("not_a_package") - p = testdir.makepyfile( + pytester.mkdir("not_a_package") + p = pytester.makepyfile( """ try: from not_a_package import doesnt_exist @@ -419,20 +428,22 @@ def test_whatever(): pass """ ) - res = testdir.runpytest(p.basename) + res = pytester.runpytest(p.name) assert res.ret == 0 - def test_unknown_option(self, testdir): - result = testdir.runpytest("--qwlkej") + def test_unknown_option(self, pytester: Pytester) -> None: + result = pytester.runpytest("--qwlkej") result.stderr.fnmatch_lines( """ *unrecognized* """ ) - def test_getsourcelines_error_issue553(self, testdir, monkeypatch): + def test_getsourcelines_error_issue553( + self, pytester: Pytester, monkeypatch + ) -> None: monkeypatch.setattr("inspect.getsourcelines", None) - p = testdir.makepyfile( + p = pytester.makepyfile( """ def raise_error(obj): raise OSError('source code not available') @@ -444,26 +455,28 @@ def test_foo(invalid_fixture): pass """ ) - res = testdir.runpytest(p) + res = pytester.runpytest(p) res.stdout.fnmatch_lines( ["*source code not available*", "E*fixture 'invalid_fixture' not found"] ) - def test_plugins_given_as_strings(self, tmpdir, monkeypatch, _sys_snapshot): + def test_plugins_given_as_strings( + self, pytester: Pytester, monkeypatch, _sys_snapshot + ) -> None: """Test that str values passed to main() as `plugins` arg are interpreted as module names to be imported and registered (#855).""" with pytest.raises(ImportError) as excinfo: - pytest.main([str(tmpdir)], plugins=["invalid.module"]) + pytest.main([str(pytester.path)], plugins=["invalid.module"]) assert "invalid" in str(excinfo.value) - p = tmpdir.join("test_test_plugins_given_as_strings.py") - p.write("def test_foo(): pass") + p = pytester.path.joinpath("test_test_plugins_given_as_strings.py") + p.write_text("def test_foo(): pass") mod = types.ModuleType("myplugin") monkeypatch.setitem(sys.modules, "myplugin", mod) - assert pytest.main(args=[str(tmpdir)], plugins=["myplugin"]) == 0 + assert pytest.main(args=[str(pytester.path)], plugins=["myplugin"]) == 0 - def test_parametrized_with_bytes_regex(self, testdir): - p = testdir.makepyfile( + def test_parametrized_with_bytes_regex(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import re import pytest @@ -472,12 +485,12 @@ def test_stuff(r): pass """ ) - res = testdir.runpytest(p) + res = pytester.runpytest(p) res.stdout.fnmatch_lines(["*1 passed*"]) - def test_parametrized_with_null_bytes(self, testdir): + def test_parametrized_with_null_bytes(self, pytester: Pytester) -> None: """Test parametrization with values that contain null bytes and unicode characters (#2644, #2957)""" - p = testdir.makepyfile( + p = pytester.makepyfile( """\ import pytest @@ -486,30 +499,30 @@ def test_foo(data): assert data """ ) - res = testdir.runpytest(p) + res = pytester.runpytest(p) res.assert_outcomes(passed=3) class TestInvocationVariants: - def test_earlyinit(self, testdir): - p = testdir.makepyfile( + def test_earlyinit(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest assert hasattr(pytest, 'mark') """ ) - result = testdir.runpython(p) + result = pytester.runpython(p) assert result.ret == 0 - def test_pydoc(self, testdir): + def test_pydoc(self, pytester: Pytester) -> None: for name in ("py.test", "pytest"): - result = testdir.runpython_c(f"import {name};help({name})") + result = pytester.runpython_c(f"import {name};help({name})") assert result.ret == 0 s = result.stdout.str() assert "MarkGenerator" in s - def test_import_star_py_dot_test(self, testdir): - p = testdir.makepyfile( + def test_import_star_py_dot_test(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ from py.test import * #collect @@ -522,11 +535,11 @@ def test_import_star_py_dot_test(self, testdir): xfail """ ) - result = testdir.runpython(p) + result = pytester.runpython(p) assert result.ret == 0 - def test_import_star_pytest(self, testdir): - p = testdir.makepyfile( + def test_import_star_pytest(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ from pytest import * #Item @@ -536,39 +549,39 @@ def test_import_star_pytest(self, testdir): xfail """ ) - result = testdir.runpython(p) + result = pytester.runpython(p) assert result.ret == 0 - def test_double_pytestcmdline(self, testdir): - p = testdir.makepyfile( + def test_double_pytestcmdline(self, pytester: Pytester) -> None: + p = pytester.makepyfile( run=""" import pytest pytest.main() pytest.main() """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_hello(): pass """ ) - result = testdir.runpython(p) + result = pytester.runpython(p) result.stdout.fnmatch_lines(["*1 passed*", "*1 passed*"]) - def test_python_minus_m_invocation_ok(self, testdir): - p1 = testdir.makepyfile("def test_hello(): pass") - res = testdir.run(sys.executable, "-m", "pytest", str(p1)) + def test_python_minus_m_invocation_ok(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile("def test_hello(): pass") + res = pytester.run(sys.executable, "-m", "pytest", str(p1)) assert res.ret == 0 - def test_python_minus_m_invocation_fail(self, testdir): - p1 = testdir.makepyfile("def test_fail(): 0/0") - res = testdir.run(sys.executable, "-m", "pytest", str(p1)) + def test_python_minus_m_invocation_fail(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile("def test_fail(): 0/0") + res = pytester.run(sys.executable, "-m", "pytest", str(p1)) assert res.ret == 1 - def test_python_pytest_package(self, testdir): - p1 = testdir.makepyfile("def test_pass(): pass") - res = testdir.run(sys.executable, "-m", "pytest", str(p1)) + def test_python_pytest_package(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile("def test_pass(): pass") + res = pytester.run(sys.executable, "-m", "pytest", str(p1)) assert res.ret == 0 res.stdout.fnmatch_lines(["*1 passed*"]) @@ -582,12 +595,12 @@ def test_invoke_with_invalid_type(self) -> None: ): pytest.main("-h") # type: ignore[arg-type] - def test_invoke_with_path(self, tmpdir: py.path.local, capsys) -> None: - retcode = pytest.main(tmpdir) + def test_invoke_with_path(self, pytester: Pytester, capsys) -> None: + retcode = pytest.main([str(pytester.path)]) assert retcode == ExitCode.NO_TESTS_COLLECTED out, err = capsys.readouterr() - def test_invoke_plugin_api(self, capsys): + def test_invoke_plugin_api(self, capsys) -> None: class MyPlugin: def pytest_addoption(self, parser): parser.addoption("--myopt") @@ -596,65 +609,71 @@ def pytest_addoption(self, parser): out, err = capsys.readouterr() assert "--myopt" in out - def test_pyargs_importerror(self, testdir, monkeypatch): + def test_pyargs_importerror(self, pytester: Pytester, monkeypatch) -> None: monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False) - path = testdir.mkpydir("tpkg") - path.join("test_hello.py").write("raise ImportError") + path = pytester.mkpydir("tpkg") + path.joinpath("test_hello.py").write_text("raise ImportError") - result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True) + result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True) assert result.ret != 0 result.stdout.fnmatch_lines(["collected*0*items*/*1*error"]) - def test_pyargs_only_imported_once(self, testdir): - pkg = testdir.mkpydir("foo") - pkg.join("test_foo.py").write("print('hello from test_foo')\ndef test(): pass") - pkg.join("conftest.py").write( + def test_pyargs_only_imported_once(self, pytester: Pytester) -> None: + pkg = pytester.mkpydir("foo") + pkg.joinpath("test_foo.py").write_text( + "print('hello from test_foo')\ndef test(): pass" + ) + pkg.joinpath("conftest.py").write_text( "def pytest_configure(config): print('configuring')" ) - result = testdir.runpytest("--pyargs", "foo.test_foo", "-s", syspathinsert=True) + result = pytester.runpytest( + "--pyargs", "foo.test_foo", "-s", syspathinsert=True + ) # should only import once assert result.outlines.count("hello from test_foo") == 1 # should only configure once assert result.outlines.count("configuring") == 1 - def test_pyargs_filename_looks_like_module(self, testdir): - testdir.tmpdir.join("conftest.py").ensure() - testdir.tmpdir.join("t.py").write("def test(): pass") - result = testdir.runpytest("--pyargs", "t.py") + def test_pyargs_filename_looks_like_module(self, pytester: Pytester) -> None: + pytester.path.joinpath("conftest.py").touch() + pytester.path.joinpath("t.py").write_text("def test(): pass") + result = pytester.runpytest("--pyargs", "t.py") assert result.ret == ExitCode.OK - def test_cmdline_python_package(self, testdir, monkeypatch): + def test_cmdline_python_package(self, pytester: Pytester, monkeypatch) -> None: import warnings monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False) - path = testdir.mkpydir("tpkg") - path.join("test_hello.py").write("def test_hello(): pass") - path.join("test_world.py").write("def test_world(): pass") - result = testdir.runpytest("--pyargs", "tpkg") + path = pytester.mkpydir("tpkg") + path.joinpath("test_hello.py").write_text("def test_hello(): pass") + path.joinpath("test_world.py").write_text("def test_world(): pass") + result = pytester.runpytest("--pyargs", "tpkg") assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) - result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True) + result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) - empty_package = testdir.mkpydir("empty_package") + empty_package = pytester.mkpydir("empty_package") monkeypatch.setenv("PYTHONPATH", str(empty_package), prepend=os.pathsep) # the path which is not a package raises a warning on pypy; # no idea why only pypy and not normal python warn about it here with warnings.catch_warnings(): warnings.simplefilter("ignore", ImportWarning) - result = testdir.runpytest("--pyargs", ".") + result = pytester.runpytest("--pyargs", ".") assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) - monkeypatch.setenv("PYTHONPATH", str(testdir), prepend=os.pathsep) - result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True) + monkeypatch.setenv("PYTHONPATH", str(pytester), prepend=os.pathsep) + result = pytester.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True) assert result.ret != 0 result.stderr.fnmatch_lines(["*not*found*test_missing*"]) - def test_cmdline_python_namespace_package(self, testdir, monkeypatch): + def test_cmdline_python_namespace_package( + self, pytester: Pytester, monkeypatch + ) -> None: """Test --pyargs option with namespace packages (#1567). Ref: https://packaging.python.org/guides/packaging-namespace-packages/ @@ -663,15 +682,17 @@ def test_cmdline_python_namespace_package(self, testdir, monkeypatch): search_path = [] for dirname in "hello", "world": - d = testdir.mkdir(dirname) + d = pytester.mkdir(dirname) search_path.append(d) - ns = d.mkdir("ns_pkg") - ns.join("__init__.py").write( + ns = d.joinpath("ns_pkg") + ns.mkdir() + ns.joinpath("__init__.py").write_text( "__import__('pkg_resources').declare_namespace(__name__)" ) - lib = ns.mkdir(dirname) - lib.ensure("__init__.py") - lib.join(f"test_{dirname}.py").write( + lib = ns.joinpath(dirname) + lib.mkdir() + lib.joinpath("__init__.py").touch() + lib.joinpath(f"test_{dirname}.py").write_text( f"def test_{dirname}(): pass\ndef test_other():pass" ) @@ -697,7 +718,7 @@ def test_cmdline_python_namespace_package(self, testdir, monkeypatch): # mixed module and filenames: monkeypatch.chdir("world") - result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world") + result = pytester.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world") assert result.ret == 0 result.stdout.fnmatch_lines( [ @@ -710,8 +731,8 @@ def test_cmdline_python_namespace_package(self, testdir, monkeypatch): ) # specify tests within a module - testdir.chdir() - result = testdir.runpytest( + pytester.chdir() + result = pytester.runpytest( "--pyargs", "-v", "ns_pkg.world.test_world::test_other" ) assert result.ret == 0 @@ -719,17 +740,19 @@ def test_cmdline_python_namespace_package(self, testdir, monkeypatch): ["*test_world.py::test_other*PASSED*", "*1 passed*"] ) - def test_invoke_test_and_doctestmodules(self, testdir): - p = testdir.makepyfile( + def test_invoke_test_and_doctestmodules(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def test(): pass """ ) - result = testdir.runpytest(str(p) + "::test", "--doctest-modules") + result = pytester.runpytest(str(p) + "::test", "--doctest-modules") result.stdout.fnmatch_lines(["*1 passed*"]) - def test_cmdline_python_package_symlink(self, testdir, monkeypatch): + def test_cmdline_python_package_symlink( + self, pytester: Pytester, monkeypatch + ) -> None: """ --pyargs with packages with path containing symlink can have conftest.py in their package (#2985) @@ -737,19 +760,21 @@ def test_cmdline_python_package_symlink(self, testdir, monkeypatch): monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) dirname = "lib" - d = testdir.mkdir(dirname) - foo = d.mkdir("foo") - foo.ensure("__init__.py") - lib = foo.mkdir("bar") - lib.ensure("__init__.py") - lib.join("test_bar.py").write( + d = pytester.mkdir(dirname) + foo = d.joinpath("foo") + foo.mkdir() + foo.joinpath("__init__.py").touch() + lib = foo.joinpath("bar") + lib.mkdir() + lib.joinpath("__init__.py").touch() + lib.joinpath("test_bar.py").write_text( "def test_bar(): pass\ndef test_other(a_fixture):pass" ) - lib.join("conftest.py").write( + lib.joinpath("conftest.py").write_text( "import pytest\n@pytest.fixture\ndef a_fixture():pass" ) - d_local = testdir.mkdir("symlink_root") + d_local = pytester.mkdir("symlink_root") symlink_location = d_local / "lib" symlink_or_skip(d, symlink_location, target_is_directory=True) @@ -773,8 +798,8 @@ def test_cmdline_python_package_symlink(self, testdir, monkeypatch): # module picked up in symlink-ed directory: # It picks up symlink_root/lib/foo/bar (symlink) via sys.path. - result = testdir.runpytest("--pyargs", "-v", "foo.bar") - testdir.chdir() + result = pytester.runpytest("--pyargs", "-v", "foo.bar") + pytester.chdir() assert result.ret == 0 result.stdout.fnmatch_lines( [ @@ -784,14 +809,14 @@ def test_cmdline_python_package_symlink(self, testdir, monkeypatch): ] ) - def test_cmdline_python_package_not_exists(self, testdir): - result = testdir.runpytest("--pyargs", "tpkgwhatv") + def test_cmdline_python_package_not_exists(self, pytester: Pytester) -> None: + result = pytester.runpytest("--pyargs", "tpkgwhatv") assert result.ret result.stderr.fnmatch_lines(["ERROR*module*or*package*not*found*"]) @pytest.mark.xfail(reason="decide: feature or bug") - def test_noclass_discovery_if_not_testcase(self, testdir): - testpath = testdir.makepyfile( + def test_noclass_discovery_if_not_testcase(self, pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import unittest class TestHello(object): @@ -802,11 +827,11 @@ class RealTest(unittest.TestCase, TestHello): attr = 42 """ ) - reprec = testdir.inline_run(testpath) + reprec = pytester.inline_run(testpath) reprec.assertoutcome(passed=1) - def test_doctest_id(self, testdir): - testdir.makefile( + def test_doctest_id(self, pytester: Pytester) -> None: + pytester.makefile( ".txt", """ >>> x=3 @@ -821,16 +846,16 @@ def test_doctest_id(self, testdir): "FAILED test_doctest_id.txt::test_doctest_id.txt", "*= 1 failed in*", ] - result = testdir.runpytest(testid, "-rf", "--tb=short") + result = pytester.runpytest(testid, "-rf", "--tb=short") result.stdout.fnmatch_lines(expected_lines) # Ensure that re-running it will still handle it as # doctest.DocTestFailure, which was not the case before when # re-importing doctest, but not creating a new RUNNER_CLASS. - result = testdir.runpytest(testid, "-rf", "--tb=short") + result = pytester.runpytest(testid, "-rf", "--tb=short") result.stdout.fnmatch_lines(expected_lines) - def test_core_backward_compatibility(self): + def test_core_backward_compatibility(self) -> None: """Test backward compatibility for get_plugin_manager function. See #787.""" import _pytest.config @@ -839,7 +864,7 @@ def test_core_backward_compatibility(self): is _pytest.config.PytestPluginManager ) - def test_has_plugin(self, request): + def test_has_plugin(self, request) -> None: """Test hasplugin function of the plugin manager (#932).""" assert request.config.pluginmanager.hasplugin("python") @@ -857,9 +882,9 @@ def test_3(): timing.sleep(0.020) """ - def test_calls(self, testdir, mock_timing): - testdir.makepyfile(self.source) - result = testdir.runpytest_inprocess("--durations=10") + def test_calls(self, pytester: Pytester, mock_timing) -> None: + pytester.makepyfile(self.source) + result = pytester.runpytest_inprocess("--durations=10") assert result.ret == 0 result.stdout.fnmatch_lines_random( @@ -870,18 +895,18 @@ def test_calls(self, testdir, mock_timing): ["(8 durations < 0.005s hidden. Use -vv to show these durations.)"] ) - def test_calls_show_2(self, testdir, mock_timing): + def test_calls_show_2(self, pytester: Pytester, mock_timing) -> None: - testdir.makepyfile(self.source) - result = testdir.runpytest_inprocess("--durations=2") + pytester.makepyfile(self.source) + result = pytester.runpytest_inprocess("--durations=2") assert result.ret == 0 lines = result.stdout.get_lines_after("*slowest*durations*") assert "4 passed" in lines[2] - def test_calls_showall(self, testdir, mock_timing): - testdir.makepyfile(self.source) - result = testdir.runpytest_inprocess("--durations=0") + def test_calls_showall(self, pytester: Pytester, mock_timing) -> None: + pytester.makepyfile(self.source) + result = pytester.runpytest_inprocess("--durations=0") assert result.ret == 0 tested = "3" @@ -893,9 +918,9 @@ def test_calls_showall(self, testdir, mock_timing): else: raise AssertionError(f"not found {x} {y}") - def test_calls_showall_verbose(self, testdir, mock_timing): - testdir.makepyfile(self.source) - result = testdir.runpytest_inprocess("--durations=0", "-vv") + def test_calls_showall_verbose(self, pytester: Pytester, mock_timing) -> None: + pytester.makepyfile(self.source) + result = pytester.runpytest_inprocess("--durations=0", "-vv") assert result.ret == 0 for x in "123": @@ -906,17 +931,17 @@ def test_calls_showall_verbose(self, testdir, mock_timing): else: raise AssertionError(f"not found {x} {y}") - def test_with_deselected(self, testdir, mock_timing): - testdir.makepyfile(self.source) - result = testdir.runpytest_inprocess("--durations=2", "-k test_3") + def test_with_deselected(self, pytester: Pytester, mock_timing) -> None: + pytester.makepyfile(self.source) + result = pytester.runpytest_inprocess("--durations=2", "-k test_3") assert result.ret == 0 result.stdout.fnmatch_lines(["*durations*", "*call*test_3*"]) - def test_with_failing_collection(self, testdir, mock_timing): - testdir.makepyfile(self.source) - testdir.makepyfile(test_collecterror="""xyz""") - result = testdir.runpytest_inprocess("--durations=2", "-k test_1") + def test_with_failing_collection(self, pytester: Pytester, mock_timing) -> None: + pytester.makepyfile(self.source) + pytester.makepyfile(test_collecterror="""xyz""") + result = pytester.runpytest_inprocess("--durations=2", "-k test_1") assert result.ret == 2 result.stdout.fnmatch_lines(["*Interrupted: 1 error during collection*"]) @@ -924,9 +949,9 @@ def test_with_failing_collection(self, testdir, mock_timing): # output result.stdout.no_fnmatch_line("*duration*") - def test_with_not(self, testdir, mock_timing): - testdir.makepyfile(self.source) - result = testdir.runpytest_inprocess("-k not 1") + def test_with_not(self, pytester: Pytester, mock_timing) -> None: + pytester.makepyfile(self.source) + result = pytester.runpytest_inprocess("-k not 1") assert result.ret == 0 @@ -943,9 +968,9 @@ def test_1(setup_fixt): timing.sleep(5) """ - def test_setup_function(self, testdir, mock_timing): - testdir.makepyfile(self.source) - result = testdir.runpytest_inprocess("--durations=10") + def test_setup_function(self, pytester: Pytester, mock_timing) -> None: + pytester.makepyfile(self.source) + result = pytester.runpytest_inprocess("--durations=10") assert result.ret == 0 result.stdout.fnmatch_lines_random( @@ -957,11 +982,11 @@ def test_setup_function(self, testdir, mock_timing): ) -def test_zipimport_hook(testdir, tmpdir): +def test_zipimport_hook(pytester: Pytester) -> None: """Test package loader is being used correctly (see #1837).""" zipapp = pytest.importorskip("zipapp") - testdir.tmpdir.join("app").ensure(dir=1) - testdir.makepyfile( + pytester.path.joinpath("app").mkdir() + pytester.makepyfile( **{ "app/foo.py": """ import pytest @@ -970,25 +995,27 @@ def main(): """ } ) - target = tmpdir.join("foo.zip") - zipapp.create_archive(str(testdir.tmpdir.join("app")), str(target), main="foo:main") - result = testdir.runpython(target) + target = pytester.path.joinpath("foo.zip") + zipapp.create_archive( + str(pytester.path.joinpath("app")), str(target), main="foo:main" + ) + result = pytester.runpython(target) assert result.ret == 0 result.stderr.fnmatch_lines(["*not found*foo*"]) result.stdout.no_fnmatch_line("*INTERNALERROR>*") -def test_import_plugin_unicode_name(testdir): - testdir.makepyfile(myplugin="") - testdir.makepyfile("def test(): pass") - testdir.makeconftest("pytest_plugins = ['myplugin']") - r = testdir.runpytest() +def test_import_plugin_unicode_name(pytester: Pytester) -> None: + pytester.makepyfile(myplugin="") + pytester.makepyfile("def test(): pass") + pytester.makeconftest("pytest_plugins = ['myplugin']") + r = pytester.runpytest() assert r.ret == 0 -def test_pytest_plugins_as_module(testdir): +def test_pytest_plugins_as_module(pytester: Pytester) -> None: """Do not raise an error if pytest_plugins attribute is a module (#3899)""" - testdir.makepyfile( + pytester.makepyfile( **{ "__init__.py": "", "pytest_plugins.py": "", @@ -996,14 +1023,14 @@ def test_pytest_plugins_as_module(testdir): "test_foo.py": "def test(): pass", } ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 1 passed in *"]) -def test_deferred_hook_checking(testdir): +def test_deferred_hook_checking(pytester: Pytester) -> None: """Check hooks as late as possible (#1821).""" - testdir.syspathinsert() - testdir.makepyfile( + pytester.syspathinsert() + pytester.makepyfile( **{ "plugin.py": """ class Hooks(object): @@ -1024,15 +1051,15 @@ def test(request): """, } ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 1 passed *"]) -def test_fixture_values_leak(testdir): +def test_fixture_values_leak(pytester: Pytester) -> None: """Ensure that fixture objects are properly destroyed by the garbage collector at the end of their expected life-times (#2981). """ - testdir.makepyfile( + pytester.makepyfile( """ import attr import gc @@ -1072,13 +1099,13 @@ def test2(): # Running on subprocess does not activate the HookRecorder # which holds itself a reference to objects in case of the # pytest_assert_reprcompare hook - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["* 2 passed *"]) -def test_fixture_order_respects_scope(testdir): +def test_fixture_order_respects_scope(pytester: Pytester) -> None: """Ensure that fixtures are created according to scope order (#2405).""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -1097,11 +1124,11 @@ def test_value(): assert data.get('value') """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 -def test_frame_leak_on_failing_test(testdir): +def test_frame_leak_on_failing_test(pytester: Pytester) -> None: """Pytest would leak garbage referencing the frames of tests that failed that could never be reclaimed (#2798). @@ -1109,7 +1136,7 @@ def test_frame_leak_on_failing_test(testdir): are made of traceback objects which cannot be weakly referenced. Those objects at least can be eventually claimed by the garbage collector. """ - testdir.makepyfile( + pytester.makepyfile( """ import gc import weakref @@ -1130,28 +1157,28 @@ def test2(): assert ref() is None """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"]) -def test_fixture_mock_integration(testdir): +def test_fixture_mock_integration(pytester: Pytester) -> None: """Test that decorators applied to fixture are left working (#3774)""" - p = testdir.copy_example("acceptance/fixture_mock_integration.py") - result = testdir.runpytest(p) + p = pytester.copy_example("acceptance/fixture_mock_integration.py") + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*1 passed*"]) -def test_usage_error_code(testdir): - result = testdir.runpytest("-unknown-option-") +def test_usage_error_code(pytester: Pytester) -> None: + result = pytester.runpytest("-unknown-option-") assert result.ret == ExitCode.USAGE_ERROR @pytest.mark.filterwarnings("default") -def test_warn_on_async_function(testdir): +def test_warn_on_async_function(pytester: Pytester) -> None: # In the below we .close() the coroutine only to avoid # "RuntimeWarning: coroutine 'test_2' was never awaited" # which messes with other tests. - testdir.makepyfile( + pytester.makepyfile( test_async=""" async def test_1(): pass @@ -1163,7 +1190,7 @@ def test_3(): return coro """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "test_async.py::test_1", @@ -1180,8 +1207,8 @@ def test_3(): @pytest.mark.filterwarnings("default") -def test_warn_on_async_gen_function(testdir): - testdir.makepyfile( +def test_warn_on_async_gen_function(pytester: Pytester) -> None: + pytester.makepyfile( test_async=""" async def test_1(): yield @@ -1191,7 +1218,7 @@ def test_3(): return test_2() """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "test_async.py::test_1", @@ -1207,8 +1234,8 @@ def test_3(): ) -def test_pdb_can_be_rewritten(testdir): - testdir.makepyfile( +def test_pdb_can_be_rewritten(pytester: Pytester) -> None: + pytester.makepyfile( **{ "conftest.py": """ import pytest @@ -1228,7 +1255,7 @@ def test(): ) # Disable debugging plugin itself to avoid: # > INTERNALERROR> AttributeError: module 'pdb' has no attribute 'set_trace' - result = testdir.runpytest_subprocess("-p", "no:debugging", "-vv") + result = pytester.runpytest_subprocess("-p", "no:debugging", "-vv") result.stdout.fnmatch_lines( [ " def check():", @@ -1244,8 +1271,8 @@ def test(): assert result.ret == 1 -def test_tee_stdio_captures_and_live_prints(testdir): - testpath = testdir.makepyfile( +def test_tee_stdio_captures_and_live_prints(pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ import sys def test_simple(): @@ -1253,7 +1280,7 @@ def test_simple(): print ("@this is stderr@", file=sys.stderr) """ ) - result = testdir.runpytest_subprocess( + result = pytester.runpytest_subprocess( testpath, "--capture=tee-sys", "--junitxml=output.xml", @@ -1266,7 +1293,7 @@ def test_simple(): result.stderr.fnmatch_lines(["*@this is stderr@*"]) # now ensure the output is in the junitxml - with open(os.path.join(testdir.tmpdir.strpath, "output.xml")) as f: + with open(pytester.path.joinpath("output.xml")) as f: fullXml = f.read() assert "@this is stdout@\n" in fullXml assert "@this is stderr@\n" in fullXml From 3405c7e6a859c044080dc34dd7c5a66748d20cee Mon Sep 17 00:00:00 2001 From: Prakhar Gurunani Date: Sat, 28 Nov 2020 21:17:02 +0530 Subject: [PATCH 802/823] Add more info about skipping doctests (#8080) Co-authored-by: Bruno Oliveira --- AUTHORS | 1 + changelog/7429.doc.rst | 1 + doc/en/doctest.rst | 45 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 changelog/7429.doc.rst diff --git a/AUTHORS b/AUTHORS index 85d5b90de3d..adf1894356a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -239,6 +239,7 @@ Philipp Loose Pieter Mulder Piotr Banaszkiewicz Piotr Helm +Prakhar Gurunani Prashant Anand Prashant Sharma Pulkit Goyal diff --git a/changelog/7429.doc.rst b/changelog/7429.doc.rst new file mode 100644 index 00000000000..e6376b727b2 --- /dev/null +++ b/changelog/7429.doc.rst @@ -0,0 +1 @@ +Add more information and use cases about skipping doctests. diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index c6e34b2b1c8..f486d5a9b6d 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -253,12 +253,32 @@ Note that like the normal ``conftest.py``, the fixtures are discovered in the di Meaning that if you put your doctest with your source code, the relevant conftest.py needs to be in the same directory tree. Fixtures will not be discovered in a sibling directory tree! -Skipping tests dynamically -^^^^^^^^^^^^^^^^^^^^^^^^^^ +Skipping tests +^^^^^^^^^^^^^^ + +For the same reasons one might want to skip normal tests, it is also possible to skip +tests inside doctests. + +To skip a single check inside a doctest you can use the standard +`doctest.SKIP `__ directive: + +.. code-block:: python + + def test_random(y): + """ + >>> random.random() # doctest: +SKIP + 0.156231223 + + >>> 1 + 1 + 2 + """ -.. versionadded:: 4.4 +This will skip the first check, but not the second. + +pytest also allows using the standard pytest functions :func:`pytest.skip` and +:func:`pytest.xfail` inside doctests, which might be useful because you can +then skip/xfail tests based on external conditions: -You can use ``pytest.skip`` to dynamically skip doctests. For example: .. code-block:: text @@ -266,3 +286,20 @@ You can use ``pytest.skip`` to dynamically skip doctests. For example: >>> if sys.platform.startswith('win'): ... pytest.skip('this doctest does not work on Windows') ... + >>> import fcntl + >>> ... + +However using those functions is discouraged because it reduces the readability of the +docstring. + +.. note:: + + :func:`pytest.skip` and :func:`pytest.xfail` behave differently depending + if the doctests are in a Python file (in docstrings) or a text file containing + doctests intermingled with text: + + * Python modules (docstrings): the functions only act in that specific docstring, + letting the other docstrings in the same module execute as normal. + + * Text files: the functions will skip/xfail the checks for the rest of the entire + file. From 6a256606c65e716b9457c50d700d93eaf2b383ca Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Sat, 14 Nov 2020 21:46:40 +0000 Subject: [PATCH 803/823] Docs: Note lifetime of temporary directories Explanation: The default handling of these lifetimes is done in `tmpdir.TempPathFactory.getbasetemp`, which passes `keep=3` to `pathlib.make_numbered_dir_with_cleanup`. GH Issue: #8036 --- src/_pytest/tmpdir.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 4ca1dd6e136..52fc8164241 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -174,6 +174,11 @@ def tmpdir(tmp_path: Path) -> py.path.local: function invocation, created as a sub directory of the base temporary directory. + By default, a new base temporary directory is created each test session, + and old bases are removed after 3 sessions, to aid in debugging. If + ``--basetemp`` is used then it is cleared each session. See :ref:`base + temporary directory`. + The returned object is a `py.path.local`_ path object. .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html @@ -187,6 +192,11 @@ def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path function invocation, created as a sub directory of the base temporary directory. + By default, a new base temporary directory is created each test session, + and old bases are removed after 3 sessions, to aid in debugging. If + ``--basetemp`` is used then it is cleared each session. See :ref:`base + temporary directory`. + The returned object is a :class:`pathlib.Path` object. """ From d1cb9de211bb5ff5f26c5d8d8cbe85133060a5ef Mon Sep 17 00:00:00 2001 From: Shubham Adep Date: Mon, 30 Nov 2020 07:48:08 -0800 Subject: [PATCH 804/823] Custom multiple marker execution order (#8065) * Custom multiple marker execution order https://github.com/pytest-dev/pytest/issues/8020 issue stated that ordering of multiple custom markers is from inside - out. I have added example for the same in the documentation. Please let me know for further changes / concerns. * remove trailing spaces The last commit was failing due to extra spaces * Ran tox tests locally to debug white space trimming issues * Resolve: ERROR: docs: commands failed for tox -e docs * Update doc/en/reference.rst Committed PR suggestions. Co-authored-by: Florian Bruhin * Added reference to Node.iter_markers_with_node in documentation * Add myself to Authors Co-authored-by: Shubham Co-authored-by: Florian Bruhin --- AUTHORS | 1 + doc/en/reference.rst | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/AUTHORS b/AUTHORS index adf1894356a..72391122eb5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -271,6 +271,7 @@ Sankt Petersbug Segev Finer Serhii Mozghovyi Seth Junot +Shubham Adep Simon Gomizelj Simon Kerr Skylar Downes diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 971ab1bef16..2bb8ed5d225 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -248,6 +248,16 @@ Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to mark.args == (10, "slow") mark.kwargs == {"method": "thread"} +Example for using multiple custom markers: + +.. code-block:: python + + @pytest.mark.timeout(10, "slow", method="thread") + @pytest.mark.slow + def test_function(): + ... + +When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``. .. _`fixtures-api`: From d4c81ffab4cbf33f4ec98fb266520c40d3edf81e Mon Sep 17 00:00:00 2001 From: Christine Mecklenborg Date: Mon, 30 Nov 2020 16:07:26 -0600 Subject: [PATCH 805/823] Migrate test_nose.py from testdir to pytester --- testing/test_nose.py | 113 ++++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 56 deletions(-) diff --git a/testing/test_nose.py b/testing/test_nose.py index b6200c6c9ad..13429afafd4 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -1,12 +1,13 @@ import pytest +from _pytest.pytester import Pytester def setup_module(mod): mod.nose = pytest.importorskip("nose") -def test_nose_setup(testdir): - p = testdir.makepyfile( +def test_nose_setup(pytester: Pytester) -> None: + p = pytester.makepyfile( """ values = [] from nose.tools import with_setup @@ -22,11 +23,11 @@ def test_world(): test_hello.teardown = lambda: values.append(2) """ ) - result = testdir.runpytest(p, "-p", "nose") + result = pytester.runpytest(p, "-p", "nose") result.assert_outcomes(passed=2) -def test_setup_func_with_setup_decorator(): +def test_setup_func_with_setup_decorator() -> None: from _pytest.nose import call_optional values = [] @@ -40,7 +41,7 @@ def f(self): assert not values -def test_setup_func_not_callable(): +def test_setup_func_not_callable() -> None: from _pytest.nose import call_optional class A: @@ -49,8 +50,8 @@ class A: call_optional(A(), "f") -def test_nose_setup_func(testdir): - p = testdir.makepyfile( +def test_nose_setup_func(pytester: Pytester) -> None: + p = pytester.makepyfile( """ from nose.tools import with_setup @@ -75,12 +76,12 @@ def test_world(): """ ) - result = testdir.runpytest(p, "-p", "nose") + result = pytester.runpytest(p, "-p", "nose") result.assert_outcomes(passed=2) -def test_nose_setup_func_failure(testdir): - p = testdir.makepyfile( +def test_nose_setup_func_failure(pytester: Pytester) -> None: + p = pytester.makepyfile( """ from nose.tools import with_setup @@ -99,12 +100,12 @@ def test_world(): """ ) - result = testdir.runpytest(p, "-p", "nose") + result = pytester.runpytest(p, "-p", "nose") result.stdout.fnmatch_lines(["*TypeError: ()*"]) -def test_nose_setup_func_failure_2(testdir): - testdir.makepyfile( +def test_nose_setup_func_failure_2(pytester: Pytester) -> None: + pytester.makepyfile( """ values = [] @@ -118,13 +119,13 @@ def test_hello(): test_hello.teardown = my_teardown """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) -def test_nose_setup_partial(testdir): +def test_nose_setup_partial(pytester: Pytester) -> None: pytest.importorskip("functools") - p = testdir.makepyfile( + p = pytester.makepyfile( """ from functools import partial @@ -153,12 +154,12 @@ def test_world(): test_hello.teardown = my_teardown_partial """ ) - result = testdir.runpytest(p, "-p", "nose") + result = pytester.runpytest(p, "-p", "nose") result.stdout.fnmatch_lines(["*2 passed*"]) -def test_module_level_setup(testdir): - testdir.makepyfile( +def test_module_level_setup(pytester: Pytester) -> None: + pytester.makepyfile( """ from nose.tools import with_setup items = {} @@ -184,12 +185,12 @@ def test_local_setup(): assert 1 not in items """ ) - result = testdir.runpytest("-p", "nose") + result = pytester.runpytest("-p", "nose") result.stdout.fnmatch_lines(["*2 passed*"]) -def test_nose_style_setup_teardown(testdir): - testdir.makepyfile( +def test_nose_style_setup_teardown(pytester: Pytester) -> None: + pytester.makepyfile( """ values = [] @@ -206,12 +207,12 @@ def test_world(): assert values == [1] """ ) - result = testdir.runpytest("-p", "nose") + result = pytester.runpytest("-p", "nose") result.stdout.fnmatch_lines(["*2 passed*"]) -def test_nose_setup_ordering(testdir): - testdir.makepyfile( +def test_nose_setup_ordering(pytester: Pytester) -> None: + pytester.makepyfile( """ def setup_module(mod): mod.visited = True @@ -223,14 +224,14 @@ def test_first(self): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) -def test_apiwrapper_problem_issue260(testdir): +def test_apiwrapper_problem_issue260(pytester: Pytester) -> None: # this would end up trying a call an optional teardown on the class # for plain unittests we don't want nose behaviour - testdir.makepyfile( + pytester.makepyfile( """ import unittest class TestCase(unittest.TestCase): @@ -248,14 +249,14 @@ def test_fun(self): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=1) -def test_setup_teardown_linking_issue265(testdir): +def test_setup_teardown_linking_issue265(pytester: Pytester) -> None: # we accidentally didn't integrate nose setupstate with normal setupstate # this test ensures that won't happen again - testdir.makepyfile( + pytester.makepyfile( ''' import pytest @@ -276,12 +277,12 @@ def teardown(self): raise Exception("should not call teardown for skipped tests") ''' ) - reprec = testdir.runpytest() + reprec = pytester.runpytest() reprec.assert_outcomes(passed=1, skipped=1) -def test_SkipTest_during_collection(testdir): - p = testdir.makepyfile( +def test_SkipTest_during_collection(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import nose raise nose.SkipTest("during collection") @@ -289,12 +290,12 @@ def test_failing(): assert False """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.assert_outcomes(skipped=1) -def test_SkipTest_in_test(testdir): - testdir.makepyfile( +def test_SkipTest_in_test(pytester: Pytester) -> None: + pytester.makepyfile( """ import nose @@ -302,12 +303,12 @@ def test_skipping(): raise nose.SkipTest("in test") """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(skipped=1) -def test_istest_function_decorator(testdir): - p = testdir.makepyfile( +def test_istest_function_decorator(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import nose.tools @nose.tools.istest @@ -315,12 +316,12 @@ def not_test_prefix(): pass """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.assert_outcomes(passed=1) -def test_nottest_function_decorator(testdir): - testdir.makepyfile( +def test_nottest_function_decorator(pytester: Pytester) -> None: + pytester.makepyfile( """ import nose.tools @nose.tools.nottest @@ -328,14 +329,14 @@ def test_prefix(): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() assert not reprec.getfailedcollections() calls = reprec.getreports("pytest_runtest_logreport") assert not calls -def test_istest_class_decorator(testdir): - p = testdir.makepyfile( +def test_istest_class_decorator(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import nose.tools @nose.tools.istest @@ -344,12 +345,12 @@ def test_method(self): pass """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.assert_outcomes(passed=1) -def test_nottest_class_decorator(testdir): - testdir.makepyfile( +def test_nottest_class_decorator(pytester: Pytester) -> None: + pytester.makepyfile( """ import nose.tools @nose.tools.nottest @@ -358,14 +359,14 @@ def test_method(self): pass """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() assert not reprec.getfailedcollections() calls = reprec.getreports("pytest_runtest_logreport") assert not calls -def test_skip_test_with_unicode(testdir): - testdir.makepyfile( +def test_skip_test_with_unicode(pytester: Pytester) -> None: + pytester.makepyfile( """\ import unittest class TestClass(): @@ -373,12 +374,12 @@ def test_io(self): raise unittest.SkipTest('😊') """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["* 1 skipped *"]) -def test_raises(testdir): - testdir.makepyfile( +def test_raises(pytester: Pytester) -> None: + pytester.makepyfile( """ from nose.tools import raises @@ -395,7 +396,7 @@ def test_raises_baseexception_caught(): raise BaseException """ ) - result = testdir.runpytest("-vv") + result = pytester.runpytest("-vv") result.stdout.fnmatch_lines( [ "test_raises.py::test_raises_runtimeerror PASSED*", From 4abd71121dad4931e13bbe4d9fb404628294572d Mon Sep 17 00:00:00 2001 From: Christine Mecklenborg Date: Mon, 30 Nov 2020 16:27:39 -0600 Subject: [PATCH 806/823] Migrate test_parseopt.py from testdir to pytester --- testing/test_parseopt.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 4d63d99eeab..a124009c401 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -9,6 +9,8 @@ import pytest from _pytest.config import argparsing as parseopt from _pytest.config.exceptions import UsageError +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Pytester @pytest.fixture @@ -287,7 +289,7 @@ def test_multiple_metavar_help(self, parser: parseopt.Parser) -> None: assert "--preferences=value1 value2 value3" in help -def test_argcomplete(testdir, monkeypatch) -> None: +def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: try: bash_version = subprocess.run( ["bash", "--version"], @@ -302,7 +304,7 @@ def test_argcomplete(testdir, monkeypatch) -> None: # See #7518. pytest.skip("not a real bash") - script = str(testdir.tmpdir.join("test_argcomplete")) + script = str(pytester.path.joinpath("test_argcomplete")) with open(str(script), "w") as fp: # redirect output from argcomplete to stdin and stderr is not trivial @@ -323,7 +325,7 @@ def test_argcomplete(testdir, monkeypatch) -> None: arg = "--fu" monkeypatch.setenv("COMP_LINE", "pytest " + arg) monkeypatch.setenv("COMP_POINT", str(len("pytest " + arg))) - result = testdir.run("bash", str(script), arg) + result = pytester.run("bash", str(script), arg) if result.ret == 255: # argcomplete not found pytest.skip("argcomplete not available") @@ -339,5 +341,5 @@ def test_argcomplete(testdir, monkeypatch) -> None: arg = "test_argc" monkeypatch.setenv("COMP_LINE", "pytest " + arg) monkeypatch.setenv("COMP_POINT", str(len("pytest " + arg))) - result = testdir.run("bash", str(script), arg) + result = pytester.run("bash", str(script), arg) result.stdout.fnmatch_lines(["test_argcomplete", "test_argcomplete.d/"]) From 7a06bc24161b7bd36c2cb6ce287eef441f002374 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Dec 2020 14:06:38 +0200 Subject: [PATCH 807/823] build(deps): bump pytest-flakes in /testing/plugins_integration (#8087) Bumps [pytest-flakes](https://github.com/asmeurer/pytest-flakes) from 4.0.2 to 4.0.3. - [Release notes](https://github.com/asmeurer/pytest-flakes/releases) - [Commits](https://github.com/asmeurer/pytest-flakes/commits) 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 4ffa8f7a1e1..477bcf12e8c 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -4,7 +4,7 @@ pytest-asyncio==0.14.0 pytest-bdd==4.0.1 pytest-cov==2.10.1 pytest-django==4.1.0 -pytest-flakes==4.0.2 +pytest-flakes==4.0.3 pytest-html==3.0.0 pytest-mock==3.3.1 pytest-rerunfailures==9.1.1 From eeb3afb8abb14e3ab6cf6bae89a1b75093e3e6d1 Mon Sep 17 00:00:00 2001 From: Christine Mecklenborg Date: Tue, 1 Dec 2020 12:55:59 -0600 Subject: [PATCH 808/823] Migrate test_pastebin.py from testdir to pytester --- testing/test_pastebin.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 2a22f405627..eaa9e7511c7 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -3,6 +3,8 @@ from typing import Union import pytest +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Pytester class TestPasteCapture: @@ -13,11 +15,11 @@ def pastebinlist(self, monkeypatch, request) -> List[Union[str, bytes]]: monkeypatch.setattr(plugin, "create_new_paste", pastebinlist.append) return pastebinlist - def test_failed(self, testdir, pastebinlist): - testpath = testdir.makepyfile( + def test_failed(self, pytester: Pytester, pastebinlist) -> None: + testpath = pytester.makepyfile( """ import pytest - def test_pass(): + def test_pass() -> None: pass def test_fail(): assert 0 @@ -25,16 +27,16 @@ def test_skip(): pytest.skip("") """ ) - reprec = testdir.inline_run(testpath, "--pastebin=failed") + reprec = pytester.inline_run(testpath, "--pastebin=failed") assert len(pastebinlist) == 1 s = pastebinlist[0] assert s.find("def test_fail") != -1 assert reprec.countoutcomes() == [1, 1, 1] - def test_all(self, testdir, pastebinlist): + def test_all(self, pytester: Pytester, pastebinlist) -> None: from _pytest.pytester import LineMatcher - testpath = testdir.makepyfile( + testpath = pytester.makepyfile( """ import pytest def test_pass(): @@ -45,7 +47,7 @@ def test_skip(): pytest.skip("") """ ) - reprec = testdir.inline_run(testpath, "--pastebin=all", "-v") + reprec = pytester.inline_run(testpath, "--pastebin=all", "-v") assert reprec.countoutcomes() == [1, 1, 1] assert len(pastebinlist) == 1 contents = pastebinlist[0].decode("utf-8") @@ -59,17 +61,17 @@ def test_skip(): ] ) - def test_non_ascii_paste_text(self, testdir, pastebinlist): + def test_non_ascii_paste_text(self, pytester: Pytester, pastebinlist) -> None: """Make sure that text which contains non-ascii characters is pasted correctly. See #1219. """ - testdir.makepyfile( + pytester.makepyfile( test_unicode="""\ def test(): assert '☺' == 1 """ ) - result = testdir.runpytest("--pastebin=all") + result = pytester.runpytest("--pastebin=all") expected_msg = "*assert '☺' == 1*" result.stdout.fnmatch_lines( [ @@ -87,7 +89,7 @@ def pastebin(self, request): return request.config.pluginmanager.getplugin("pastebin") @pytest.fixture - def mocked_urlopen_fail(self, monkeypatch): + def mocked_urlopen_fail(self, monkeypatch: MonkeyPatch): """Monkeypatch the actual urlopen call to emulate a HTTP Error 400.""" calls = [] @@ -102,7 +104,7 @@ def mocked(url, data): return calls @pytest.fixture - def mocked_urlopen_invalid(self, monkeypatch): + def mocked_urlopen_invalid(self, monkeypatch: MonkeyPatch): """Monkeypatch the actual urlopen calls done by the internal plugin function that connects to bpaste service, but return a url in an unexpected format.""" @@ -124,7 +126,7 @@ def read(self): return calls @pytest.fixture - def mocked_urlopen(self, monkeypatch): + def mocked_urlopen(self, monkeypatch: MonkeyPatch): """Monkeypatch the actual urlopen calls done by the internal plugin function that connects to bpaste service.""" calls = [] @@ -144,7 +146,7 @@ def read(self): monkeypatch.setattr(urllib.request, "urlopen", mocked) return calls - def test_pastebin_invalid_url(self, pastebin, mocked_urlopen_invalid): + def test_pastebin_invalid_url(self, pastebin, mocked_urlopen_invalid) -> None: result = pastebin.create_new_paste(b"full-paste-contents") assert ( result @@ -152,12 +154,12 @@ def test_pastebin_invalid_url(self, pastebin, mocked_urlopen_invalid): ) assert len(mocked_urlopen_invalid) == 1 - def test_pastebin_http_error(self, pastebin, mocked_urlopen_fail): + def test_pastebin_http_error(self, pastebin, mocked_urlopen_fail) -> None: result = pastebin.create_new_paste(b"full-paste-contents") assert result == "bad response: HTTP Error 400: Bad request" assert len(mocked_urlopen_fail) == 1 - def test_create_new_paste(self, pastebin, mocked_urlopen): + def test_create_new_paste(self, pastebin, mocked_urlopen) -> None: result = pastebin.create_new_paste(b"full-paste-contents") assert result == "https://bpaste.net/show/3c0c6750bd" assert len(mocked_urlopen) == 1 @@ -169,7 +171,7 @@ def test_create_new_paste(self, pastebin, mocked_urlopen): assert "code=full-paste-contents" in data.decode() assert "expiry=1week" in data.decode() - def test_create_new_paste_failure(self, pastebin, monkeypatch): + def test_create_new_paste_failure(self, pastebin, monkeypatch: MonkeyPatch) -> None: import io import urllib.request From 4fc20c8d28b637974afa237a89e8956252a794a2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 4 Dec 2020 15:24:56 -0300 Subject: [PATCH 809/823] List pytest-doctestplus in doctest docs As per https://github.com/pytest-dev/pytest/discussions/8088 --- doc/en/doctest.rst | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index f486d5a9b6d..f8d010679f0 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -77,15 +77,6 @@ putting them into a pytest.ini file like this: [pytest] addopts = --doctest-modules -.. note:: - - The builtin pytest doctest supports only ``doctest`` blocks, but if you are looking - for more advanced checking over *all* your documentation, - including doctests, ``.. codeblock:: python`` Sphinx directive support, - and any other examples your documentation may include, you may wish to - consider `Sybil `__. - It provides pytest integration out of the box. - Encoding -------- @@ -303,3 +294,18 @@ docstring. * Text files: the functions will skip/xfail the checks for the rest of the entire file. + + +Alternatives +------------ + +While the built-in pytest support provides a good set of functionalities for using +doctests, if you use them extensively you might be interested in those external packages +which add many more features, and include pytest integration: + +* `pytest-doctestplus `__: provides + advanced doctest support and enables the testing of reStructuredText (".rst") files. + +* `Sybil `__: provides a way to test examples in + your documentation by parsing them from the documentation source and evaluating + the parsed examples as part of your normal test run. From 8c120c042cbb30a22547f8a10b1da55ad5057076 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Dec 2020 03:08:29 +0000 Subject: [PATCH 810/823] build(deps): bump django in /testing/plugins_integration Bumps [django](https://github.com/django/django) from 3.1.3 to 3.1.4. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.1.3...3.1.4) 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 477bcf12e8c..5646e8e9255 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,5 +1,5 @@ anyio[curio,trio]==2.0.2 -django==3.1.3 +django==3.1.4 pytest-asyncio==0.14.0 pytest-bdd==4.0.1 pytest-cov==2.10.1 From 19de6bccffb890bbf4326ac2a2a76f59b42e04ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Dec 2020 03:08:29 +0000 Subject: [PATCH 811/823] build(deps): bump pytest-html in /testing/plugins_integration Bumps [pytest-html](https://github.com/pytest-dev/pytest-html) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/pytest-dev/pytest-html/releases) - [Changelog](https://github.com/pytest-dev/pytest-html/blob/master/CHANGES.rst) - [Commits](https://github.com/pytest-dev/pytest-html/compare/v3.0.0...v3.1.0) 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 477bcf12e8c..cd7685d9d19 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -5,7 +5,7 @@ pytest-bdd==4.0.1 pytest-cov==2.10.1 pytest-django==4.1.0 pytest-flakes==4.0.3 -pytest-html==3.0.0 +pytest-html==3.1.0 pytest-mock==3.3.1 pytest-rerunfailures==9.1.1 pytest-sugar==0.9.4 From 810b878ef8eac6f39cfff35705a6a10083ace0bc Mon Sep 17 00:00:00 2001 From: Anton <44246099+antonblr@users.noreply.github.com> Date: Tue, 8 Dec 2020 12:20:02 -0800 Subject: [PATCH 812/823] Migrate to pytester: test_capture.py, test_terminal.py, approx.py (#8108) * Migrate to pytester: test_capture.py, test_config.py, approx.py * migrate test_terminal.py * revert test_config.py * more typing in test_terminal.py * try-out 'tr' fixture update * revert 'tr' fixture, update test_config.py --- testing/python/approx.py | 7 +- testing/test_capture.py | 388 +++++++++++----------- testing/test_config.py | 654 +++++++++++++++++++----------------- testing/test_terminal.py | 701 ++++++++++++++++++++------------------- 4 files changed, 928 insertions(+), 822 deletions(-) diff --git a/testing/python/approx.py b/testing/python/approx.py index b37c9f757d0..91c1f3f85de 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -7,6 +7,7 @@ from typing import Optional import pytest +from _pytest.pytester import Pytester from pytest import approx inf, nan = float("inf"), float("nan") @@ -456,12 +457,12 @@ def test_doctests(self, mocked_doctest_runner) -> None: ) mocked_doctest_runner.run(test) - def test_unicode_plus_minus(self, testdir): + def test_unicode_plus_minus(self, pytester: Pytester) -> None: """ Comparing approx instances inside lists should not produce an error in the detailed diff. Integration test for issue #2111. """ - testdir.makepyfile( + pytester.makepyfile( """ import pytest def test_foo(): @@ -469,7 +470,7 @@ def test_foo(): """ ) expected = "4.0e-06" - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [f"*At index 0 diff: 3 != 4 ± {expected}", "=* 1 failed in *="] ) diff --git a/testing/test_capture.py b/testing/test_capture.py index e6bbc9a5d19..3a5c617fe5a 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -13,11 +13,13 @@ import pytest from _pytest import capture from _pytest.capture import _get_multicapture +from _pytest.capture import CaptureFixture from _pytest.capture import CaptureManager from _pytest.capture import CaptureResult from _pytest.capture import MultiCapture from _pytest.config import ExitCode -from _pytest.pytester import Testdir +from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Pytester # note: py.io capture tests where copied from # pylib 1.4.20.dev2 (rev 13d9af95547e) @@ -55,7 +57,7 @@ def TeeStdCapture( class TestCaptureManager: @pytest.mark.parametrize("method", ["no", "sys", "fd"]) - def test_capturing_basic_api(self, method): + def test_capturing_basic_api(self, method) -> None: capouter = StdCaptureFD() old = sys.stdout, sys.stderr, sys.stdin try: @@ -96,9 +98,9 @@ def test_init_capturing(self): @pytest.mark.parametrize("method", ["fd", "sys"]) -def test_capturing_unicode(testdir, method): +def test_capturing_unicode(pytester: Pytester, method: str) -> None: obj = "'b\u00f6y'" - testdir.makepyfile( + pytester.makepyfile( """\ # taken from issue 227 from nosetests def test_unicode(): @@ -108,24 +110,24 @@ def test_unicode(): """ % obj ) - result = testdir.runpytest("--capture=%s" % method) + result = pytester.runpytest("--capture=%s" % method) result.stdout.fnmatch_lines(["*1 passed*"]) @pytest.mark.parametrize("method", ["fd", "sys"]) -def test_capturing_bytes_in_utf8_encoding(testdir, method): - testdir.makepyfile( +def test_capturing_bytes_in_utf8_encoding(pytester: Pytester, method: str) -> None: + pytester.makepyfile( """\ def test_unicode(): print('b\\u00f6y') """ ) - result = testdir.runpytest("--capture=%s" % method) + result = pytester.runpytest("--capture=%s" % method) result.stdout.fnmatch_lines(["*1 passed*"]) -def test_collect_capturing(testdir): - p = testdir.makepyfile( +def test_collect_capturing(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import sys @@ -134,7 +136,7 @@ def test_collect_capturing(testdir): import xyz42123 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines( [ "*Captured stdout*", @@ -146,8 +148,8 @@ def test_collect_capturing(testdir): class TestPerTestCapturing: - def test_capture_and_fixtures(self, testdir): - p = testdir.makepyfile( + def test_capture_and_fixtures(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def setup_module(mod): print("setup module") @@ -161,7 +163,7 @@ def test_func2(): assert 0 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines( [ "setup module*", @@ -173,8 +175,8 @@ def test_func2(): ) @pytest.mark.xfail(reason="unimplemented feature") - def test_capture_scope_cache(self, testdir): - p = testdir.makepyfile( + def test_capture_scope_cache(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ import sys def setup_module(func): @@ -188,7 +190,7 @@ def teardown_function(func): print("in teardown") """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines( [ "*test_func():*", @@ -200,8 +202,8 @@ def teardown_function(func): ] ) - def test_no_carry_over(self, testdir): - p = testdir.makepyfile( + def test_no_carry_over(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def test_func1(): print("in func1") @@ -210,13 +212,13 @@ def test_func2(): assert 0 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) s = result.stdout.str() assert "in func1" not in s assert "in func2" in s - def test_teardown_capturing(self, testdir): - p = testdir.makepyfile( + def test_teardown_capturing(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def setup_function(function): print("setup func1") @@ -228,7 +230,7 @@ def test_func1(): pass """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines( [ "*teardown_function*", @@ -240,8 +242,8 @@ def test_func1(): ] ) - def test_teardown_capturing_final(self, testdir): - p = testdir.makepyfile( + def test_teardown_capturing_final(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def teardown_module(mod): print("teardown module") @@ -250,7 +252,7 @@ def test_func(): pass """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines( [ "*def teardown_module(mod):*", @@ -260,8 +262,8 @@ def test_func(): ] ) - def test_capturing_outerr(self, testdir): - p1 = testdir.makepyfile( + def test_capturing_outerr(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """\ import sys def test_capturing(): @@ -273,7 +275,7 @@ def test_capturing_error(): raise ValueError """ ) - result = testdir.runpytest(p1) + result = pytester.runpytest(p1) result.stdout.fnmatch_lines( [ "*test_capturing_outerr.py .F*", @@ -289,8 +291,8 @@ def test_capturing_error(): class TestLoggingInteraction: - def test_logging_stream_ownership(self, testdir): - p = testdir.makepyfile( + def test_logging_stream_ownership(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """\ def test_logging(): import logging @@ -300,11 +302,11 @@ def test_logging(): stream.close() # to free memory/release resources """ ) - result = testdir.runpytest_subprocess(p) + result = pytester.runpytest_subprocess(p) assert result.stderr.str().find("atexit") == -1 - def test_logging_and_immediate_setupteardown(self, testdir): - p = testdir.makepyfile( + def test_logging_and_immediate_setupteardown(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """\ import logging def setup_function(function): @@ -321,7 +323,7 @@ def teardown_function(function): ) for optargs in (("--capture=sys",), ("--capture=fd",)): print(optargs) - result = testdir.runpytest_subprocess(p, *optargs) + result = pytester.runpytest_subprocess(p, *optargs) s = result.stdout.str() result.stdout.fnmatch_lines( ["*WARN*hello3", "*WARN*hello1", "*WARN*hello2"] # errors show first! @@ -329,8 +331,8 @@ def teardown_function(function): # verify proper termination assert "closed" not in s - def test_logging_and_crossscope_fixtures(self, testdir): - p = testdir.makepyfile( + def test_logging_and_crossscope_fixtures(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """\ import logging def setup_module(function): @@ -347,7 +349,7 @@ def teardown_module(function): ) for optargs in (("--capture=sys",), ("--capture=fd",)): print(optargs) - result = testdir.runpytest_subprocess(p, *optargs) + result = pytester.runpytest_subprocess(p, *optargs) s = result.stdout.str() result.stdout.fnmatch_lines( ["*WARN*hello3", "*WARN*hello1", "*WARN*hello2"] # errors come first @@ -355,8 +357,8 @@ def teardown_module(function): # verify proper termination assert "closed" not in s - def test_conftestlogging_is_shown(self, testdir): - testdir.makeconftest( + def test_conftestlogging_is_shown(self, pytester: Pytester) -> None: + pytester.makeconftest( """\ import logging logging.basicConfig() @@ -364,20 +366,20 @@ def test_conftestlogging_is_shown(self, testdir): """ ) # make sure that logging is still captured in tests - result = testdir.runpytest_subprocess("-s", "-p", "no:capturelog") + result = pytester.runpytest_subprocess("-s", "-p", "no:capturelog") assert result.ret == ExitCode.NO_TESTS_COLLECTED result.stderr.fnmatch_lines(["WARNING*hello435*"]) assert "operation on closed file" not in result.stderr.str() - def test_conftestlogging_and_test_logging(self, testdir): - testdir.makeconftest( + def test_conftestlogging_and_test_logging(self, pytester: Pytester) -> None: + pytester.makeconftest( """\ import logging logging.basicConfig() """ ) # make sure that logging is still captured in tests - p = testdir.makepyfile( + p = pytester.makepyfile( """\ def test_hello(): import logging @@ -385,14 +387,14 @@ def test_hello(): assert 0 """ ) - result = testdir.runpytest_subprocess(p, "-p", "no:capturelog") + result = pytester.runpytest_subprocess(p, "-p", "no:capturelog") assert result.ret != 0 result.stdout.fnmatch_lines(["WARNING*hello433*"]) assert "something" not in result.stderr.str() assert "operation on closed file" not in result.stderr.str() - def test_logging_after_cap_stopped(self, testdir): - testdir.makeconftest( + def test_logging_after_cap_stopped(self, pytester: Pytester) -> None: + pytester.makeconftest( """\ import pytest import logging @@ -406,7 +408,7 @@ def log_on_teardown(): """ ) # make sure that logging is still captured in tests - p = testdir.makepyfile( + p = pytester.makepyfile( """\ def test_hello(log_on_teardown): import logging @@ -415,7 +417,7 @@ def test_hello(log_on_teardown): raise KeyboardInterrupt() """ ) - result = testdir.runpytest_subprocess(p, "--log-cli-level", "info") + result = pytester.runpytest_subprocess(p, "--log-cli-level", "info") assert result.ret != 0 result.stdout.fnmatch_lines( ["*WARNING*hello433*", "*WARNING*Logging on teardown*"] @@ -428,8 +430,8 @@ def test_hello(log_on_teardown): class TestCaptureFixture: @pytest.mark.parametrize("opt", [[], ["-s"]]) - def test_std_functional(self, testdir, opt): - reprec = testdir.inline_runsource( + def test_std_functional(self, pytester: Pytester, opt) -> None: + reprec = pytester.inline_runsource( """\ def test_hello(capsys): print(42) @@ -440,8 +442,8 @@ def test_hello(capsys): ) reprec.assertoutcome(passed=1) - def test_capsyscapfd(self, testdir): - p = testdir.makepyfile( + def test_capsyscapfd(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """\ def test_one(capsys, capfd): pass @@ -449,7 +451,7 @@ def test_two(capfd, capsys): pass """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines( [ "*ERROR*setup*test_one*", @@ -460,11 +462,11 @@ def test_two(capfd, capsys): ] ) - def test_capturing_getfixturevalue(self, testdir): + def test_capturing_getfixturevalue(self, pytester: Pytester) -> None: """Test that asking for "capfd" and "capsys" using request.getfixturevalue in the same test is an error. """ - testdir.makepyfile( + pytester.makepyfile( """\ def test_one(capsys, request): request.getfixturevalue("capfd") @@ -472,7 +474,7 @@ def test_two(capfd, request): request.getfixturevalue("capsys") """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*test_one*", @@ -483,21 +485,23 @@ def test_two(capfd, request): ] ) - def test_capsyscapfdbinary(self, testdir): - p = testdir.makepyfile( + def test_capsyscapfdbinary(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """\ def test_one(capsys, capfdbinary): pass """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines( ["*ERROR*setup*test_one*", "E*capfdbinary*capsys*same*time*", "*1 error*"] ) @pytest.mark.parametrize("method", ["sys", "fd"]) - def test_capture_is_represented_on_failure_issue128(self, testdir, method): - p = testdir.makepyfile( + def test_capture_is_represented_on_failure_issue128( + self, pytester: Pytester, method + ) -> None: + p = pytester.makepyfile( """\ def test_hello(cap{}): print("xxx42xxx") @@ -506,11 +510,11 @@ def test_hello(cap{}): method ) ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["xxx42xxx"]) - def test_stdfd_functional(self, testdir): - reprec = testdir.inline_runsource( + def test_stdfd_functional(self, pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """\ def test_hello(capfd): import os @@ -523,13 +527,13 @@ def test_hello(capfd): reprec.assertoutcome(passed=1) @pytest.mark.parametrize("nl", ("\n", "\r\n", "\r")) - def test_cafd_preserves_newlines(self, capfd, nl): + def test_cafd_preserves_newlines(self, capfd, nl) -> None: print("test", end=nl) out, err = capfd.readouterr() assert out.endswith(nl) - def test_capfdbinary(self, testdir): - reprec = testdir.inline_runsource( + def test_capfdbinary(self, pytester: Pytester) -> None: + reprec = pytester.inline_runsource( """\ def test_hello(capfdbinary): import os @@ -542,8 +546,8 @@ def test_hello(capfdbinary): ) reprec.assertoutcome(passed=1) - def test_capsysbinary(self, testdir): - p1 = testdir.makepyfile( + def test_capsysbinary(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( r""" def test_hello(capsysbinary): import sys @@ -567,7 +571,7 @@ def test_hello(capsysbinary): print("stderr after", file=sys.stderr) """ ) - result = testdir.runpytest(str(p1), "-rA") + result = pytester.runpytest(str(p1), "-rA") result.stdout.fnmatch_lines( [ "*- Captured stdout call -*", @@ -578,18 +582,18 @@ def test_hello(capsysbinary): ] ) - def test_partial_setup_failure(self, testdir): - p = testdir.makepyfile( + def test_partial_setup_failure(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """\ def test_hello(capsys, missingarg): pass """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*test_partial_setup_failure*", "*1 error*"]) - def test_keyboardinterrupt_disables_capturing(self, testdir): - p = testdir.makepyfile( + def test_keyboardinterrupt_disables_capturing(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """\ def test_hello(capfd): import os @@ -597,26 +601,28 @@ def test_hello(capfd): raise KeyboardInterrupt() """ ) - result = testdir.runpytest_subprocess(p) + result = pytester.runpytest_subprocess(p) result.stdout.fnmatch_lines(["*KeyboardInterrupt*"]) assert result.ret == 2 - def test_capture_and_logging(self, testdir): + def test_capture_and_logging(self, pytester: Pytester) -> None: """#14""" - p = testdir.makepyfile( + p = pytester.makepyfile( """\ import logging def test_log(capsys): logging.error('x') """ ) - result = testdir.runpytest_subprocess(p) + result = pytester.runpytest_subprocess(p) assert "closed" not in result.stderr.str() @pytest.mark.parametrize("fixture", ["capsys", "capfd"]) @pytest.mark.parametrize("no_capture", [True, False]) - def test_disabled_capture_fixture(self, testdir, fixture, no_capture): - testdir.makepyfile( + def test_disabled_capture_fixture( + self, pytester: Pytester, fixture: str, no_capture: bool + ) -> None: + pytester.makepyfile( """\ def test_disabled({fixture}): print('captured before') @@ -632,7 +638,7 @@ def test_normal(): ) ) args = ("-s",) if no_capture else () - result = testdir.runpytest_subprocess(*args) + result = pytester.runpytest_subprocess(*args) result.stdout.fnmatch_lines(["*while capture is disabled*", "*= 2 passed in *"]) result.stdout.no_fnmatch_line("*captured before*") result.stdout.no_fnmatch_line("*captured after*") @@ -641,12 +647,12 @@ def test_normal(): else: result.stdout.no_fnmatch_line("*test_normal executed*") - def test_disabled_capture_fixture_twice(self, testdir: Testdir) -> None: + def test_disabled_capture_fixture_twice(self, pytester: Pytester) -> None: """Test that an inner disabled() exit doesn't undo an outer disabled(). Issue #7148. """ - testdir.makepyfile( + pytester.makepyfile( """ def test_disabled(capfd): print('captured before') @@ -659,7 +665,7 @@ def test_disabled(capfd): assert capfd.readouterr() == ('captured before\\ncaptured after\\n', '') """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines( [ "*while capture is disabled 1", @@ -670,10 +676,10 @@ def test_disabled(capfd): ) @pytest.mark.parametrize("fixture", ["capsys", "capfd"]) - def test_fixture_use_by_other_fixtures(self, testdir, fixture): + def test_fixture_use_by_other_fixtures(self, pytester: Pytester, fixture) -> None: """Ensure that capsys and capfd can be used by other fixtures during setup and teardown.""" - testdir.makepyfile( + pytester.makepyfile( """\ import sys import pytest @@ -700,15 +706,17 @@ def test_captured_print(captured_print): fixture=fixture ) ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 passed*"]) result.stdout.no_fnmatch_line("*stdout contents begin*") result.stdout.no_fnmatch_line("*stderr contents begin*") @pytest.mark.parametrize("cap", ["capsys", "capfd"]) - def test_fixture_use_by_other_fixtures_teardown(self, testdir, cap): + def test_fixture_use_by_other_fixtures_teardown( + self, pytester: Pytester, cap + ) -> None: """Ensure we can access setup and teardown buffers from teardown when using capsys/capfd (##3033)""" - testdir.makepyfile( + pytester.makepyfile( """\ import sys import pytest @@ -730,13 +738,13 @@ def test_a(fix): cap=cap ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) -def test_setup_failure_does_not_kill_capturing(testdir): - sub1 = testdir.mkpydir("sub1") - sub1.join("conftest.py").write( +def test_setup_failure_does_not_kill_capturing(pytester: Pytester) -> None: + sub1 = pytester.mkpydir("sub1") + sub1.joinpath("conftest.py").write_text( textwrap.dedent( """\ def pytest_runtest_setup(item): @@ -744,26 +752,26 @@ def pytest_runtest_setup(item): """ ) ) - sub1.join("test_mod.py").write("def test_func1(): pass") - result = testdir.runpytest(testdir.tmpdir, "--traceconfig") + sub1.joinpath("test_mod.py").write_text("def test_func1(): pass") + result = pytester.runpytest(pytester.path, "--traceconfig") result.stdout.fnmatch_lines(["*ValueError(42)*", "*1 error*"]) -def test_capture_conftest_runtest_setup(testdir): - testdir.makeconftest( +def test_capture_conftest_runtest_setup(pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_runtest_setup(): print("hello19") """ ) - testdir.makepyfile("def test_func(): pass") - result = testdir.runpytest() + pytester.makepyfile("def test_func(): pass") + result = pytester.runpytest() assert result.ret == 0 result.stdout.no_fnmatch_line("*hello19*") -def test_capture_badoutput_issue412(testdir): - testdir.makepyfile( +def test_capture_badoutput_issue412(pytester: Pytester) -> None: + pytester.makepyfile( """ import os @@ -773,7 +781,7 @@ def test_func(): assert 0 """ ) - result = testdir.runpytest("--capture=fd") + result = pytester.runpytest("--capture=fd") result.stdout.fnmatch_lines( """ *def test_func* @@ -784,21 +792,21 @@ def test_func(): ) -def test_capture_early_option_parsing(testdir): - testdir.makeconftest( +def test_capture_early_option_parsing(pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_runtest_setup(): print("hello19") """ ) - testdir.makepyfile("def test_func(): pass") - result = testdir.runpytest("-vs") + pytester.makepyfile("def test_func(): pass") + result = pytester.runpytest("-vs") assert result.ret == 0 assert "hello19" in result.stdout.str() -def test_capture_binary_output(testdir): - testdir.makepyfile( +def test_capture_binary_output(pytester: Pytester) -> None: + pytester.makepyfile( r""" import pytest @@ -814,13 +822,13 @@ def test_foo(): test_foo() """ ) - result = testdir.runpytest("--assert=plain") + result = pytester.runpytest("--assert=plain") result.assert_outcomes(passed=2) -def test_error_during_readouterr(testdir): +def test_error_during_readouterr(pytester: Pytester) -> None: """Make sure we suspend capturing if errors occur during readouterr""" - testdir.makepyfile( + pytester.makepyfile( pytest_xyz=""" from _pytest.capture import FDCapture @@ -831,26 +839,26 @@ def bad_snap(self): FDCapture.snap = bad_snap """ ) - result = testdir.runpytest_subprocess("-p", "pytest_xyz", "--version") + result = pytester.runpytest_subprocess("-p", "pytest_xyz", "--version") result.stderr.fnmatch_lines( ["*in bad_snap", " raise Exception('boom')", "Exception: boom"] ) class TestCaptureIO: - def test_text(self): + def test_text(self) -> None: f = capture.CaptureIO() f.write("hello") s = f.getvalue() assert s == "hello" f.close() - def test_unicode_and_str_mixture(self): + def test_unicode_and_str_mixture(self) -> None: f = capture.CaptureIO() f.write("\u00f6") pytest.raises(TypeError, f.write, b"hello") - def test_write_bytes_to_buffer(self): + def test_write_bytes_to_buffer(self) -> None: """In python3, stdout / stderr are text io wrappers (exposing a buffer property of the underlying bytestream). See issue #1407 """ @@ -860,7 +868,7 @@ def test_write_bytes_to_buffer(self): class TestTeeCaptureIO(TestCaptureIO): - def test_text(self): + def test_text(self) -> None: sio = io.StringIO() f = capture.TeeCaptureIO(sio) f.write("hello") @@ -871,14 +879,14 @@ def test_text(self): f.close() sio.close() - def test_unicode_and_str_mixture(self): + def test_unicode_and_str_mixture(self) -> None: sio = io.StringIO() f = capture.TeeCaptureIO(sio) f.write("\u00f6") pytest.raises(TypeError, f.write, b"hello") -def test_dontreadfrominput(): +def test_dontreadfrominput() -> None: from _pytest.capture import DontReadFromInput f = DontReadFromInput() @@ -923,8 +931,8 @@ def test_captureresult() -> None: @pytest.fixture -def tmpfile(testdir) -> Generator[BinaryIO, None, None]: - f = testdir.makepyfile("").open("wb+") +def tmpfile(pytester: Pytester) -> Generator[BinaryIO, None, None]: + f = pytester.makepyfile("").open("wb+") yield f if not f.closed: f.close() @@ -946,7 +954,7 @@ def lsof_check(): class TestFDCapture: - def test_simple(self, tmpfile): + def test_simple(self, tmpfile: BinaryIO) -> None: fd = tmpfile.fileno() cap = capture.FDCapture(fd) data = b"hello" @@ -960,22 +968,22 @@ def test_simple(self, tmpfile): cap.done() assert s == "hello" - def test_simple_many(self, tmpfile): + def test_simple_many(self, tmpfile: BinaryIO) -> None: for i in range(10): self.test_simple(tmpfile) - def test_simple_many_check_open_files(self, testdir): + def test_simple_many_check_open_files(self, pytester: Pytester) -> None: with lsof_check(): - with testdir.makepyfile("").open("wb+") as tmpfile: + with pytester.makepyfile("").open("wb+") as tmpfile: self.test_simple_many(tmpfile) - def test_simple_fail_second_start(self, tmpfile): + def test_simple_fail_second_start(self, tmpfile: BinaryIO) -> None: fd = tmpfile.fileno() cap = capture.FDCapture(fd) cap.done() pytest.raises(AssertionError, cap.start) - def test_stderr(self): + def test_stderr(self) -> None: cap = capture.FDCapture(2) cap.start() print("hello", file=sys.stderr) @@ -983,14 +991,14 @@ def test_stderr(self): cap.done() assert s == "hello\n" - def test_stdin(self): + def test_stdin(self) -> None: cap = capture.FDCapture(0) cap.start() x = os.read(0, 100).strip() cap.done() assert x == b"" - def test_writeorg(self, tmpfile): + def test_writeorg(self, tmpfile: BinaryIO) -> None: data1, data2 = b"foo", b"bar" cap = capture.FDCapture(tmpfile.fileno()) cap.start() @@ -1004,7 +1012,7 @@ def test_writeorg(self, tmpfile): stmp = stmp_file.read() assert stmp == data2 - def test_simple_resume_suspend(self): + def test_simple_resume_suspend(self) -> None: with saved_fd(1): cap = capture.FDCapture(1) cap.start() @@ -1038,7 +1046,7 @@ def test_simple_resume_suspend(self): ) ) - def test_capfd_sys_stdout_mode(self, capfd): + def test_capfd_sys_stdout_mode(self, capfd) -> None: assert "b" not in sys.stdout.mode @@ -1064,7 +1072,7 @@ def getcapture(self, **kw): finally: cap.stop_capturing() - def test_capturing_done_simple(self): + def test_capturing_done_simple(self) -> None: with self.getcapture() as cap: sys.stdout.write("hello") sys.stderr.write("world") @@ -1072,7 +1080,7 @@ def test_capturing_done_simple(self): assert out == "hello" assert err == "world" - def test_capturing_reset_simple(self): + def test_capturing_reset_simple(self) -> None: with self.getcapture() as cap: print("hello world") sys.stderr.write("hello error\n") @@ -1080,7 +1088,7 @@ def test_capturing_reset_simple(self): assert out == "hello world\n" assert err == "hello error\n" - def test_capturing_readouterr(self): + def test_capturing_readouterr(self) -> None: with self.getcapture() as cap: print("hello world") sys.stderr.write("hello error\n") @@ -1091,7 +1099,7 @@ def test_capturing_readouterr(self): out, err = cap.readouterr() assert err == "error2" - def test_capture_results_accessible_by_attribute(self): + def test_capture_results_accessible_by_attribute(self) -> None: with self.getcapture() as cap: sys.stdout.write("hello") sys.stderr.write("world") @@ -1099,13 +1107,13 @@ def test_capture_results_accessible_by_attribute(self): assert capture_result.out == "hello" assert capture_result.err == "world" - def test_capturing_readouterr_unicode(self): + def test_capturing_readouterr_unicode(self) -> None: with self.getcapture() as cap: print("hxąć") out, err = cap.readouterr() assert out == "hxąć\n" - def test_reset_twice_error(self): + def test_reset_twice_error(self) -> None: with self.getcapture() as cap: print("hello") out, err = cap.readouterr() @@ -1113,7 +1121,7 @@ def test_reset_twice_error(self): assert out == "hello\n" assert not err - def test_capturing_modify_sysouterr_in_between(self): + def test_capturing_modify_sysouterr_in_between(self) -> None: oldout = sys.stdout olderr = sys.stderr with self.getcapture() as cap: @@ -1129,7 +1137,7 @@ def test_capturing_modify_sysouterr_in_between(self): assert sys.stdout == oldout assert sys.stderr == olderr - def test_capturing_error_recursive(self): + def test_capturing_error_recursive(self) -> None: with self.getcapture() as cap1: print("cap1") with self.getcapture() as cap2: @@ -1139,7 +1147,7 @@ def test_capturing_error_recursive(self): assert out1 == "cap1\n" assert out2 == "cap2\n" - def test_just_out_capture(self): + def test_just_out_capture(self) -> None: with self.getcapture(out=True, err=False) as cap: sys.stdout.write("hello") sys.stderr.write("world") @@ -1147,7 +1155,7 @@ def test_just_out_capture(self): assert out == "hello" assert not err - def test_just_err_capture(self): + def test_just_err_capture(self) -> None: with self.getcapture(out=False, err=True) as cap: sys.stdout.write("hello") sys.stderr.write("world") @@ -1155,14 +1163,14 @@ def test_just_err_capture(self): assert err == "world" assert not out - def test_stdin_restored(self): + def test_stdin_restored(self) -> None: old = sys.stdin with self.getcapture(in_=True): newstdin = sys.stdin assert newstdin != sys.stdin assert sys.stdin is old - def test_stdin_nulled_by_default(self): + def test_stdin_nulled_by_default(self) -> None: print("XXX this test may well hang instead of crashing") print("XXX which indicates an error in the underlying capturing") print("XXX mechanisms") @@ -1173,7 +1181,7 @@ def test_stdin_nulled_by_default(self): class TestTeeStdCapture(TestStdCapture): captureclass = staticmethod(TeeStdCapture) - def test_capturing_error_recursive(self): + def test_capturing_error_recursive(self) -> None: r"""For TeeStdCapture since we passthrough stderr/stdout, cap1 should get all output, while cap2 should only get "cap2\n".""" @@ -1190,8 +1198,8 @@ def test_capturing_error_recursive(self): class TestStdCaptureFD(TestStdCapture): captureclass = staticmethod(StdCaptureFD) - def test_simple_only_fd(self, testdir): - testdir.makepyfile( + def test_simple_only_fd(self, pytester: Pytester) -> None: + pytester.makepyfile( """\ import os def test_x(): @@ -1199,7 +1207,7 @@ def test_x(): assert 0 """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines( """ *test_x* @@ -1231,8 +1239,8 @@ def test_many(self, capfd): class TestStdCaptureFDinvalidFD: - def test_stdcapture_fd_invalid_fd(self, testdir): - testdir.makepyfile( + def test_stdcapture_fd_invalid_fd(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import os from fnmatch import fnmatch @@ -1270,11 +1278,11 @@ def test_stdin(): cap.stop_capturing() """ ) - result = testdir.runpytest_subprocess("--capture=fd") + result = pytester.runpytest_subprocess("--capture=fd") assert result.ret == 0 assert result.parseoutcomes()["passed"] == 3 - def test_fdcapture_invalid_fd_with_fd_reuse(self, testdir): + def test_fdcapture_invalid_fd_with_fd_reuse(self, pytester: Pytester) -> None: with saved_fd(1): os.close(1) cap = capture.FDCaptureBinary(1) @@ -1289,7 +1297,7 @@ def test_fdcapture_invalid_fd_with_fd_reuse(self, testdir): with pytest.raises(OSError): os.write(1, b"done") - def test_fdcapture_invalid_fd_without_fd_reuse(self, testdir): + def test_fdcapture_invalid_fd_without_fd_reuse(self, pytester: Pytester) -> None: with saved_fd(1), saved_fd(2): os.close(1) os.close(2) @@ -1306,12 +1314,14 @@ def test_fdcapture_invalid_fd_without_fd_reuse(self, testdir): os.write(2, b"done") -def test_capture_not_started_but_reset(): +def test_capture_not_started_but_reset() -> None: capsys = StdCapture() capsys.stop_capturing() -def test_using_capsys_fixture_works_with_sys_stdout_encoding(capsys): +def test_using_capsys_fixture_works_with_sys_stdout_encoding( + capsys: CaptureFixture[str], +) -> None: test_text = "test text" print(test_text.encode(sys.stdout.encoding, "replace")) @@ -1320,7 +1330,7 @@ def test_using_capsys_fixture_works_with_sys_stdout_encoding(capsys): assert err == "" -def test_capsys_results_accessible_by_attribute(capsys): +def test_capsys_results_accessible_by_attribute(capsys: CaptureFixture[str]) -> None: sys.stdout.write("spam") sys.stderr.write("eggs") capture_result = capsys.readouterr() @@ -1340,8 +1350,8 @@ def test_fdcapture_tmpfile_remains_the_same() -> None: assert capfile2 == capfile -def test_close_and_capture_again(testdir): - testdir.makepyfile( +def test_close_and_capture_again(pytester: Pytester) -> None: + pytester.makepyfile( """ import os def test_close(): @@ -1351,7 +1361,7 @@ def test_capture_again(): assert 0 """ ) - result = testdir.runpytest_subprocess() + result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines( """ *test_capture_again* @@ -1365,9 +1375,9 @@ def test_capture_again(): @pytest.mark.parametrize( "method", ["SysCapture(2)", "SysCapture(2, tee=True)", "FDCapture(2)"] ) -def test_capturing_and_logging_fundamentals(testdir, method: str) -> None: +def test_capturing_and_logging_fundamentals(pytester: Pytester, method: str) -> None: # here we check a fundamental feature - p = testdir.makepyfile( + p = pytester.makepyfile( """ import sys, os import py, logging @@ -1392,7 +1402,7 @@ def test_capturing_and_logging_fundamentals(testdir, method: str) -> None: """ % (method,) ) - result = testdir.runpython(p) + result = pytester.runpython(p) result.stdout.fnmatch_lines( """ suspend, captured*hello1* @@ -1407,8 +1417,8 @@ def test_capturing_and_logging_fundamentals(testdir, method: str) -> None: assert "atexit" not in result.stderr.str() -def test_error_attribute_issue555(testdir): - testdir.makepyfile( +def test_error_attribute_issue555(pytester: Pytester) -> None: + pytester.makepyfile( """ import sys def test_capattr(): @@ -1416,7 +1426,7 @@ def test_capattr(): assert sys.stderr.errors == "replace" """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -1438,8 +1448,8 @@ def write(self, s): _py36_windowsconsoleio_workaround(stream) -def test_dontreadfrominput_has_encoding(testdir): - testdir.makepyfile( +def test_dontreadfrominput_has_encoding(pytester: Pytester) -> None: + pytester.makepyfile( """ import sys def test_capattr(): @@ -1448,12 +1458,14 @@ def test_capattr(): assert sys.stderr.encoding """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) -def test_crash_on_closing_tmpfile_py27(testdir): - p = testdir.makepyfile( +def test_crash_on_closing_tmpfile_py27( + pytester: Pytester, monkeypatch: MonkeyPatch +) -> None: + p = pytester.makepyfile( """ import threading import sys @@ -1480,19 +1492,19 @@ def test_spam_in_thread(): """ ) # Do not consider plugins like hypothesis, which might output to stderr. - testdir.monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") - result = testdir.runpytest_subprocess(str(p)) + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") + result = pytester.runpytest_subprocess(str(p)) assert result.ret == 0 assert result.stderr.str() == "" result.stdout.no_fnmatch_line("*OSError*") -def test_global_capture_with_live_logging(testdir): +def test_global_capture_with_live_logging(pytester: Pytester) -> None: # Issue 3819 # capture should work with live cli logging # Teardown report seems to have the capture for the whole process (setup, capture, teardown) - testdir.makeconftest( + pytester.makeconftest( """ def pytest_runtest_logreport(report): if "test_global" in report.nodeid: @@ -1504,7 +1516,7 @@ def pytest_runtest_logreport(report): """ ) - testdir.makepyfile( + pytester.makepyfile( """ import logging import sys @@ -1526,7 +1538,7 @@ def test_global(fix1): print("end test") """ ) - result = testdir.runpytest_subprocess("--log-cli-level=INFO") + result = pytester.runpytest_subprocess("--log-cli-level=INFO") assert result.ret == 0 with open("caplog") as f: @@ -1546,11 +1558,13 @@ def test_global(fix1): @pytest.mark.parametrize("capture_fixture", ["capsys", "capfd"]) -def test_capture_with_live_logging(testdir, capture_fixture): +def test_capture_with_live_logging( + pytester: Pytester, capture_fixture: CaptureFixture[str] +) -> None: # Issue 3819 # capture should work with live cli logging - testdir.makepyfile( + pytester.makepyfile( """ import logging import sys @@ -1575,21 +1589,21 @@ def test_capture({0}): ) ) - result = testdir.runpytest_subprocess("--log-cli-level=INFO") + result = pytester.runpytest_subprocess("--log-cli-level=INFO") assert result.ret == 0 -def test_typeerror_encodedfile_write(testdir): +def test_typeerror_encodedfile_write(pytester: Pytester) -> None: """It should behave the same with and without output capturing (#4861).""" - p = testdir.makepyfile( + p = pytester.makepyfile( """ def test_fails(): import sys sys.stdout.write(b"foo") """ ) - result_without_capture = testdir.runpytest("-s", str(p)) - result_with_capture = testdir.runpytest(str(p)) + result_without_capture = pytester.runpytest("-s", str(p)) + result_with_capture = pytester.runpytest(str(p)) assert result_with_capture.ret == result_without_capture.ret out = result_with_capture.stdout.str() @@ -1598,7 +1612,7 @@ def test_fails(): ) -def test_stderr_write_returns_len(capsys): +def test_stderr_write_returns_len(capsys: CaptureFixture[str]) -> None: """Write on Encoded files, namely captured stderr, should return number of characters written.""" assert sys.stderr.write("Foo") == 3 @@ -1623,9 +1637,9 @@ def test__get_multicapture() -> None: ) -def test_logging_while_collecting(testdir): +def test_logging_while_collecting(pytester: Pytester) -> None: """Issue #6240: Calls to logging.xxx() during collection causes all logging calls to be duplicated to stderr""" - p = testdir.makepyfile( + p = pytester.makepyfile( """\ import logging @@ -1636,7 +1650,7 @@ def test_logging(): assert False """ ) - result = testdir.runpytest_subprocess(p) + result = pytester.runpytest_subprocess(p) assert result.ret == ExitCode.TESTS_FAILED result.stdout.fnmatch_lines( [ diff --git a/testing/test_config.py b/testing/test_config.py index f3fa6437239..b931797d429 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -8,6 +8,7 @@ from typing import Sequence from typing import Tuple from typing import Type +from typing import Union import attr import py.path @@ -27,7 +28,7 @@ from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import locate_config from _pytest.monkeypatch import MonkeyPatch -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester class TestParseIni: @@ -36,7 +37,7 @@ class TestParseIni: ) def test_getcfg_and_config( self, - testdir: Testdir, + pytester: Pytester, tmp_path: Path, section: str, filename: str, @@ -58,12 +59,12 @@ def test_getcfg_and_config( ) _, _, cfg = locate_config([sub]) assert cfg["name"] == "value" - config = testdir.parseconfigure(str(sub)) + config = pytester.parseconfigure(str(sub)) assert config.inicfg["name"] == "value" - def test_setupcfg_uses_toolpytest_with_pytest(self, testdir): - p1 = testdir.makepyfile("def test(): pass") - testdir.makefile( + def test_setupcfg_uses_toolpytest_with_pytest(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile("def test(): pass") + pytester.makefile( ".cfg", setup=""" [tool:pytest] @@ -71,15 +72,17 @@ def test_setupcfg_uses_toolpytest_with_pytest(self, testdir): [pytest] testpaths=ignored """ - % p1.basename, + % p1.name, ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*, configfile: setup.cfg, *", "* 1 passed in *"]) assert result.ret == 0 - def test_append_parse_args(self, testdir, tmpdir, monkeypatch): + def test_append_parse_args( + self, pytester: Pytester, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: monkeypatch.setenv("PYTEST_ADDOPTS", '--color no -rs --tb="short"') - tmpdir.join("pytest.ini").write( + tmp_path.joinpath("pytest.ini").write_text( textwrap.dedent( """\ [pytest] @@ -87,21 +90,21 @@ def test_append_parse_args(self, testdir, tmpdir, monkeypatch): """ ) ) - config = testdir.parseconfig(tmpdir) + config = pytester.parseconfig(tmp_path) assert config.option.color == "no" assert config.option.reportchars == "s" assert config.option.tbstyle == "short" assert config.option.verbose - def test_tox_ini_wrong_version(self, testdir): - testdir.makefile( + def test_tox_ini_wrong_version(self, pytester: Pytester) -> None: + pytester.makefile( ".ini", tox=""" [pytest] minversion=999.0 """, ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret != 0 result.stderr.fnmatch_lines( ["*tox.ini: 'minversion' requires pytest-999.0, actual pytest-*"] @@ -111,8 +114,8 @@ def test_tox_ini_wrong_version(self, testdir): "section, name", [("tool:pytest", "setup.cfg"), ("pytest", "tox.ini"), ("pytest", "pytest.ini")], ) - def test_ini_names(self, testdir, name, section): - testdir.tmpdir.join(name).write( + def test_ini_names(self, pytester: Pytester, name, section) -> None: + pytester.path.joinpath(name).write_text( textwrap.dedent( """ [{section}] @@ -122,22 +125,22 @@ def test_ini_names(self, testdir, name, section): ) ) ) - config = testdir.parseconfig() + config = pytester.parseconfig() assert config.getini("minversion") == "1.0" - def test_pyproject_toml(self, testdir): - testdir.makepyprojecttoml( + def test_pyproject_toml(self, pytester: Pytester) -> None: + pytester.makepyprojecttoml( """ [tool.pytest.ini_options] minversion = "1.0" """ ) - config = testdir.parseconfig() + config = pytester.parseconfig() assert config.getini("minversion") == "1.0" - def test_toxini_before_lower_pytestini(self, testdir): - sub = testdir.tmpdir.mkdir("sub") - sub.join("tox.ini").write( + def test_toxini_before_lower_pytestini(self, pytester: Pytester) -> None: + sub = pytester.mkdir("sub") + sub.joinpath("tox.ini").write_text( textwrap.dedent( """ [pytest] @@ -145,7 +148,7 @@ def test_toxini_before_lower_pytestini(self, testdir): """ ) ) - testdir.tmpdir.join("pytest.ini").write( + pytester.path.joinpath("pytest.ini").write_text( textwrap.dedent( """ [pytest] @@ -153,26 +156,26 @@ def test_toxini_before_lower_pytestini(self, testdir): """ ) ) - config = testdir.parseconfigure(sub) + config = pytester.parseconfigure(sub) assert config.getini("minversion") == "2.0" - def test_ini_parse_error(self, testdir): - testdir.tmpdir.join("pytest.ini").write("addopts = -x") - result = testdir.runpytest() + def test_ini_parse_error(self, pytester: Pytester) -> None: + pytester.path.joinpath("pytest.ini").write_text("addopts = -x") + result = pytester.runpytest() assert result.ret != 0 result.stderr.fnmatch_lines(["ERROR: *pytest.ini:1: no section header defined"]) @pytest.mark.xfail(reason="probably not needed") - def test_confcutdir(self, testdir): - sub = testdir.mkdir("sub") - sub.chdir() - testdir.makeini( + def test_confcutdir(self, pytester: Pytester) -> None: + sub = pytester.mkdir("sub") + os.chdir(sub) + pytester.makeini( """ [pytest] addopts = --qwe """ ) - result = testdir.inline_run("--confcutdir=.") + result = pytester.inline_run("--confcutdir=.") assert result.ret == 0 @pytest.mark.parametrize( @@ -243,24 +246,29 @@ def test_confcutdir(self, testdir): ) @pytest.mark.filterwarnings("default") def test_invalid_config_options( - self, testdir, ini_file_text, invalid_keys, warning_output, exception_text - ): - testdir.makeconftest( + self, + pytester: Pytester, + ini_file_text, + invalid_keys, + warning_output, + exception_text, + ) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("conftest_ini_key", "") """ ) - testdir.makepyfile("def test(): pass") - testdir.makeini(ini_file_text) + pytester.makepyfile("def test(): pass") + pytester.makeini(ini_file_text) - config = testdir.parseconfig() + config = pytester.parseconfig() assert sorted(config._get_unknown_ini_keys()) == sorted(invalid_keys) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(warning_output) - result = testdir.runpytest("--strict-config") + result = pytester.runpytest("--strict-config") if exception_text: result.stderr.fnmatch_lines("ERROR: " + exception_text) assert result.ret == pytest.ExitCode.USAGE_ERROR @@ -269,9 +277,9 @@ def pytest_addoption(parser): assert result.ret == pytest.ExitCode.OK @pytest.mark.filterwarnings("default") - def test_silence_unknown_key_warning(self, testdir: Testdir) -> None: + def test_silence_unknown_key_warning(self, pytester: Pytester) -> None: """Unknown config key warnings can be silenced using filterwarnings (#7620)""" - testdir.makeini( + pytester.makeini( """ [pytest] filterwarnings = @@ -279,15 +287,15 @@ def test_silence_unknown_key_warning(self, testdir: Testdir) -> None: foobar=1 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.no_fnmatch_line("*PytestConfigWarning*") @pytest.mark.filterwarnings("default") def test_disable_warnings_plugin_disables_config_warnings( - self, testdir: Testdir + self, pytester: Pytester ) -> None: """Disabling 'warnings' plugin also disables config time warnings""" - testdir.makeconftest( + pytester.makeconftest( """ import pytest def pytest_configure(config): @@ -297,7 +305,7 @@ def pytest_configure(config): ) """ ) - result = testdir.runpytest("-pno:warnings") + result = pytester.runpytest("-pno:warnings") result.stdout.no_fnmatch_line("*PytestConfigWarning*") @pytest.mark.parametrize( @@ -371,8 +379,12 @@ def pytest_configure(config): ], ) def test_missing_required_plugins( - self, testdir, monkeypatch, ini_file_text, exception_text - ): + self, + pytester: Pytester, + monkeypatch: MonkeyPatch, + ini_file_text: str, + exception_text: str, + ) -> None: """Check 'required_plugins' option with various settings. This test installs a mock "myplugin-1.5" which is used in the parametrized test cases. @@ -405,26 +417,28 @@ def metadata(self): def my_dists(): return [DummyDist(entry_points)] - testdir.makepyfile(myplugin1_module="# my plugin module") - testdir.syspathinsert() + pytester.makepyfile(myplugin1_module="# my plugin module") + pytester.syspathinsert() monkeypatch.setattr(importlib_metadata, "distributions", my_dists) - testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) - testdir.makeini(ini_file_text) + pytester.makeini(ini_file_text) if exception_text: with pytest.raises(pytest.UsageError, match=exception_text): - testdir.parseconfig() + pytester.parseconfig() else: - testdir.parseconfig() + pytester.parseconfig() - def test_early_config_cmdline(self, testdir, monkeypatch): + def test_early_config_cmdline( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: """early_config contains options registered by third-party plugins. This is a regression involving pytest-cov (and possibly others) introduced in #7700. """ - testdir.makepyfile( + pytester.makepyfile( myplugin=""" def pytest_addoption(parser): parser.addoption('--foo', default=None, dest='foo') @@ -434,50 +448,52 @@ def pytest_load_initial_conftests(early_config, parser, args): """ ) monkeypatch.setenv("PYTEST_PLUGINS", "myplugin") - testdir.syspathinsert() - result = testdir.runpytest("--foo=1") + pytester.syspathinsert() + result = pytester.runpytest("--foo=1") result.stdout.fnmatch_lines("* no tests ran in *") class TestConfigCmdlineParsing: - def test_parsing_again_fails(self, testdir): - config = testdir.parseconfig() + def test_parsing_again_fails(self, pytester: Pytester) -> None: + config = pytester.parseconfig() pytest.raises(AssertionError, lambda: config.parse([])) - def test_explicitly_specified_config_file_is_loaded(self, testdir): - testdir.makeconftest( + def test_explicitly_specified_config_file_is_loaded( + self, pytester: Pytester + ) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("custom", "") """ ) - testdir.makeini( + pytester.makeini( """ [pytest] custom = 0 """ ) - testdir.makefile( + pytester.makefile( ".ini", custom=""" [pytest] custom = 1 """, ) - config = testdir.parseconfig("-c", "custom.ini") + config = pytester.parseconfig("-c", "custom.ini") assert config.getini("custom") == "1" - testdir.makefile( + pytester.makefile( ".cfg", custom_tool_pytest_section=""" [tool:pytest] custom = 1 """, ) - config = testdir.parseconfig("-c", "custom_tool_pytest_section.cfg") + config = pytester.parseconfig("-c", "custom_tool_pytest_section.cfg") assert config.getini("custom") == "1" - testdir.makefile( + pytester.makefile( ".toml", custom=""" [tool.pytest.ini_options] @@ -486,11 +502,11 @@ def pytest_addoption(parser): ] # this is here on purpose, as it makes this an invalid '.ini' file """, ) - config = testdir.parseconfig("-c", "custom.toml") + config = pytester.parseconfig("-c", "custom.toml") assert config.getini("custom") == "1" - def test_absolute_win32_path(self, testdir): - temp_ini_file = testdir.makefile( + def test_absolute_win32_path(self, pytester: Pytester) -> None: + temp_ini_file = pytester.makefile( ".ini", custom=""" [pytest] @@ -499,103 +515,103 @@ def test_absolute_win32_path(self, testdir): ) from os.path import normpath - temp_ini_file = normpath(str(temp_ini_file)) - ret = pytest.main(["-c", temp_ini_file]) + temp_ini_file_norm = normpath(str(temp_ini_file)) + ret = pytest.main(["-c", temp_ini_file_norm]) assert ret == ExitCode.OK class TestConfigAPI: - def test_config_trace(self, testdir) -> None: - config = testdir.parseconfig() + def test_config_trace(self, pytester: Pytester) -> None: + config = pytester.parseconfig() values: List[str] = [] config.trace.root.setwriter(values.append) config.trace("hello") assert len(values) == 1 assert values[0] == "hello [config]\n" - def test_config_getoption(self, testdir): - testdir.makeconftest( + def test_config_getoption(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addoption("--hello", "-X", dest="hello") """ ) - config = testdir.parseconfig("--hello=this") + config = pytester.parseconfig("--hello=this") for x in ("hello", "--hello", "-X"): assert config.getoption(x) == "this" pytest.raises(ValueError, config.getoption, "qweqwe") - def test_config_getoption_unicode(self, testdir): - testdir.makeconftest( + def test_config_getoption_unicode(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addoption('--hello', type=str) """ ) - config = testdir.parseconfig("--hello=this") + config = pytester.parseconfig("--hello=this") assert config.getoption("hello") == "this" - def test_config_getvalueorskip(self, testdir): - config = testdir.parseconfig() + def test_config_getvalueorskip(self, pytester: Pytester) -> None: + config = pytester.parseconfig() pytest.raises(pytest.skip.Exception, config.getvalueorskip, "hello") verbose = config.getvalueorskip("verbose") assert verbose == config.option.verbose - def test_config_getvalueorskip_None(self, testdir): - testdir.makeconftest( + def test_config_getvalueorskip_None(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addoption("--hello") """ ) - config = testdir.parseconfig() + config = pytester.parseconfig() with pytest.raises(pytest.skip.Exception): config.getvalueorskip("hello") - def test_getoption(self, testdir): - config = testdir.parseconfig() + def test_getoption(self, pytester: Pytester) -> None: + config = pytester.parseconfig() with pytest.raises(ValueError): config.getvalue("x") assert config.getoption("x", 1) == 1 - def test_getconftest_pathlist(self, testdir, tmpdir): + def test_getconftest_pathlist(self, pytester: Pytester, tmpdir) -> None: somepath = tmpdir.join("x", "y", "z") p = tmpdir.join("conftest.py") p.write("pathlist = ['.', %r]" % str(somepath)) - config = testdir.parseconfigure(p) + config = pytester.parseconfigure(p) assert config._getconftest_pathlist("notexist", path=tmpdir) is None - pl = config._getconftest_pathlist("pathlist", path=tmpdir) + pl = config._getconftest_pathlist("pathlist", path=tmpdir) or [] print(pl) assert len(pl) == 2 assert pl[0] == tmpdir assert pl[1] == somepath @pytest.mark.parametrize("maybe_type", ["not passed", "None", '"string"']) - def test_addini(self, testdir, maybe_type): + def test_addini(self, pytester: Pytester, maybe_type: str) -> None: if maybe_type == "not passed": type_string = "" else: type_string = f", {maybe_type}" - testdir.makeconftest( + pytester.makeconftest( f""" def pytest_addoption(parser): parser.addini("myname", "my new ini value"{type_string}) """ ) - testdir.makeini( + pytester.makeini( """ [pytest] myname=hello """ ) - config = testdir.parseconfig() + config = pytester.parseconfig() val = config.getini("myname") assert val == "hello" pytest.raises(ValueError, config.getini, "other") - def make_conftest_for_pathlist(self, testdir): - testdir.makeconftest( + def make_conftest_for_pathlist(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("paths", "my new ini value", type="pathlist") @@ -603,36 +619,36 @@ def pytest_addoption(parser): """ ) - def test_addini_pathlist_ini_files(self, testdir): - self.make_conftest_for_pathlist(testdir) - p = testdir.makeini( + def test_addini_pathlist_ini_files(self, pytester: Pytester) -> None: + self.make_conftest_for_pathlist(pytester) + p = pytester.makeini( """ [pytest] paths=hello world/sub.py """ ) - self.check_config_pathlist(testdir, p) + self.check_config_pathlist(pytester, p) - def test_addini_pathlist_pyproject_toml(self, testdir): - self.make_conftest_for_pathlist(testdir) - p = testdir.makepyprojecttoml( + def test_addini_pathlist_pyproject_toml(self, pytester: Pytester) -> None: + self.make_conftest_for_pathlist(pytester) + p = pytester.makepyprojecttoml( """ [tool.pytest.ini_options] paths=["hello", "world/sub.py"] """ ) - self.check_config_pathlist(testdir, p) + self.check_config_pathlist(pytester, p) - def check_config_pathlist(self, testdir, config_path): - config = testdir.parseconfig() + def check_config_pathlist(self, pytester: Pytester, config_path: Path) -> None: + config = pytester.parseconfig() values = config.getini("paths") assert len(values) == 2 - assert values[0] == config_path.dirpath("hello") - assert values[1] == config_path.dirpath("world/sub.py") + assert values[0] == config_path.parent.joinpath("hello") + assert values[1] == config_path.parent.joinpath("world/sub.py") pytest.raises(ValueError, config.getini, "other") - def make_conftest_for_args(self, testdir): - testdir.makeconftest( + def make_conftest_for_args(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("args", "new args", type="args") @@ -640,35 +656,35 @@ def pytest_addoption(parser): """ ) - def test_addini_args_ini_files(self, testdir): - self.make_conftest_for_args(testdir) - testdir.makeini( + def test_addini_args_ini_files(self, pytester: Pytester) -> None: + self.make_conftest_for_args(pytester) + pytester.makeini( """ [pytest] args=123 "123 hello" "this" """ ) - self.check_config_args(testdir) + self.check_config_args(pytester) - def test_addini_args_pyproject_toml(self, testdir): - self.make_conftest_for_args(testdir) - testdir.makepyprojecttoml( + def test_addini_args_pyproject_toml(self, pytester: Pytester) -> None: + self.make_conftest_for_args(pytester) + pytester.makepyprojecttoml( """ [tool.pytest.ini_options] args = ["123", "123 hello", "this"] """ ) - self.check_config_args(testdir) + self.check_config_args(pytester) - def check_config_args(self, testdir): - config = testdir.parseconfig() + def check_config_args(self, pytester: Pytester) -> None: + config = pytester.parseconfig() values = config.getini("args") assert values == ["123", "123 hello", "this"] values = config.getini("a2") assert values == list("123") - def make_conftest_for_linelist(self, testdir): - testdir.makeconftest( + def make_conftest_for_linelist(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("xy", "", type="linelist") @@ -676,29 +692,29 @@ def pytest_addoption(parser): """ ) - def test_addini_linelist_ini_files(self, testdir): - self.make_conftest_for_linelist(testdir) - testdir.makeini( + def test_addini_linelist_ini_files(self, pytester: Pytester) -> None: + self.make_conftest_for_linelist(pytester) + pytester.makeini( """ [pytest] xy= 123 345 second line """ ) - self.check_config_linelist(testdir) + self.check_config_linelist(pytester) - def test_addini_linelist_pprojecttoml(self, testdir): - self.make_conftest_for_linelist(testdir) - testdir.makepyprojecttoml( + def test_addini_linelist_pprojecttoml(self, pytester: Pytester) -> None: + self.make_conftest_for_linelist(pytester) + pytester.makepyprojecttoml( """ [tool.pytest.ini_options] xy = ["123 345", "second line"] """ ) - self.check_config_linelist(testdir) + self.check_config_linelist(pytester) - def check_config_linelist(self, testdir): - config = testdir.parseconfig() + def check_config_linelist(self, pytester: Pytester) -> None: + config = pytester.parseconfig() values = config.getini("xy") assert len(values) == 2 assert values == ["123 345", "second line"] @@ -708,38 +724,40 @@ def check_config_linelist(self, testdir): @pytest.mark.parametrize( "str_val, bool_val", [("True", True), ("no", False), ("no-ini", True)] ) - def test_addini_bool(self, testdir, str_val, bool_val): - testdir.makeconftest( + def test_addini_bool( + self, pytester: Pytester, str_val: str, bool_val: bool + ) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("strip", "", type="bool", default=True) """ ) if str_val != "no-ini": - testdir.makeini( + pytester.makeini( """ [pytest] strip=%s """ % str_val ) - config = testdir.parseconfig() + config = pytester.parseconfig() assert config.getini("strip") is bool_val - def test_addinivalue_line_existing(self, testdir): - testdir.makeconftest( + def test_addinivalue_line_existing(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("xy", "", type="linelist") """ ) - testdir.makeini( + pytester.makeini( """ [pytest] xy= 123 """ ) - config = testdir.parseconfig() + config = pytester.parseconfig() values = config.getini("xy") assert len(values) == 1 assert values == ["123"] @@ -748,14 +766,14 @@ def pytest_addoption(parser): assert len(values) == 2 assert values == ["123", "456"] - def test_addinivalue_line_new(self, testdir): - testdir.makeconftest( + def test_addinivalue_line_new(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("xy", "", type="linelist") """ ) - config = testdir.parseconfig() + config = pytester.parseconfig() assert not config.getini("xy") config.addinivalue_line("xy", "456") values = config.getini("xy") @@ -766,19 +784,17 @@ def pytest_addoption(parser): assert len(values) == 2 assert values == ["456", "123"] - def test_confcutdir_check_isdir(self, testdir): + def test_confcutdir_check_isdir(self, pytester: Pytester) -> None: """Give an error if --confcutdir is not a valid directory (#2078)""" exp_match = r"^--confcutdir must be a directory, given: " with pytest.raises(pytest.UsageError, match=exp_match): - testdir.parseconfig( - "--confcutdir", testdir.tmpdir.join("file").ensure(file=1) - ) + pytester.parseconfig("--confcutdir", pytester.path.joinpath("file")) with pytest.raises(pytest.UsageError, match=exp_match): - testdir.parseconfig("--confcutdir", testdir.tmpdir.join("inexistant")) - config = testdir.parseconfig( - "--confcutdir", testdir.tmpdir.join("dir").ensure(dir=1) - ) - assert config.getoption("confcutdir") == str(testdir.tmpdir.join("dir")) + pytester.parseconfig("--confcutdir", pytester.path.joinpath("inexistant")) + + p = pytester.mkdir("dir") + config = pytester.parseconfig("--confcutdir", p) + assert config.getoption("confcutdir") == str(p) @pytest.mark.parametrize( "names, expected", @@ -796,12 +812,12 @@ def test_confcutdir_check_isdir(self, testdir): (["source/python/bar/__init__.py", "setup.py"], ["bar"]), ], ) - def test_iter_rewritable_modules(self, names, expected): + def test_iter_rewritable_modules(self, names, expected) -> None: assert list(_iter_rewritable_modules(names)) == expected class TestConfigFromdictargs: - def test_basic_behavior(self, _sys_snapshot): + def test_basic_behavior(self, _sys_snapshot) -> None: option_dict = {"verbose": 444, "foo": "bar", "capture": "no"} args = ["a", "b"] @@ -824,8 +840,12 @@ def test_invocation_params_args(self, _sys_snapshot) -> None: assert config.option.verbose == 4 assert config.option.capture == "no" - def test_inifilename(self, tmpdir): - tmpdir.join("foo/bar.ini").ensure().write( + def test_inifilename(self, tmp_path: Path) -> None: + d1 = tmp_path.joinpath("foo") + d1.mkdir() + p1 = d1.joinpath("bar.ini") + p1.touch() + p1.write_text( textwrap.dedent( """\ [pytest] @@ -837,8 +857,11 @@ def test_inifilename(self, tmpdir): inifile = "../../foo/bar.ini" option_dict = {"inifilename": inifile, "capture": "no"} - cwd = tmpdir.join("a/b") - cwd.join("pytest.ini").ensure().write( + cwd = tmp_path.joinpath("a/b") + cwd.mkdir(parents=True) + p2 = cwd.joinpath("pytest.ini") + p2.touch() + p2.write_text( textwrap.dedent( """\ [pytest] @@ -847,7 +870,8 @@ def test_inifilename(self, tmpdir): """ ) ) - with cwd.ensure(dir=True).as_cwd(): + with MonkeyPatch.context() as mp: + mp.chdir(cwd) config = Config.fromdictargs(option_dict, ()) inipath = py.path.local(inifile) @@ -861,18 +885,20 @@ def test_inifilename(self, tmpdir): assert config.inicfg.get("should_not_be_set") is None -def test_options_on_small_file_do_not_blow_up(testdir) -> None: +def test_options_on_small_file_do_not_blow_up(pytester: Pytester) -> None: def runfiletest(opts: Sequence[str]) -> None: - reprec = testdir.inline_run(*opts) + reprec = pytester.inline_run(*opts) passed, skipped, failed = reprec.countoutcomes() assert failed == 2 assert skipped == passed == 0 - path = testdir.makepyfile( - """ + path = str( + pytester.makepyfile( + """ def test_f1(): assert 0 def test_f2(): assert 0 """ + ) ) runfiletest([path]) @@ -887,7 +913,9 @@ def test_f2(): assert 0 runfiletest(["-v", "-v", path]) -def test_preparse_ordering_with_setuptools(testdir, monkeypatch): +def test_preparse_ordering_with_setuptools( + pytester: Pytester, monkeypatch: MonkeyPatch +) -> None: monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) class EntryPoint: @@ -909,18 +937,20 @@ def my_dists(): return (Dist,) monkeypatch.setattr(importlib_metadata, "distributions", my_dists) - testdir.makeconftest( + pytester.makeconftest( """ pytest_plugins = "mytestplugin", """ ) monkeypatch.setenv("PYTEST_PLUGINS", "mytestplugin") - config = testdir.parseconfig() + config = pytester.parseconfig() plugin = config.pluginmanager.getplugin("mytestplugin") assert plugin.x == 42 -def test_setuptools_importerror_issue1479(testdir, monkeypatch): +def test_setuptools_importerror_issue1479( + pytester: Pytester, monkeypatch: MonkeyPatch +) -> None: monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) class DummyEntryPoint: @@ -941,10 +971,12 @@ def distributions(): monkeypatch.setattr(importlib_metadata, "distributions", distributions) with pytest.raises(ImportError): - testdir.parseconfig() + pytester.parseconfig() -def test_importlib_metadata_broken_distribution(testdir, monkeypatch): +def test_importlib_metadata_broken_distribution( + pytester: Pytester, monkeypatch: MonkeyPatch +) -> None: """Integration test for broken distributions with 'files' metadata being None (#5389)""" monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) @@ -965,11 +997,13 @@ def distributions(): return (Distribution(),) monkeypatch.setattr(importlib_metadata, "distributions", distributions) - testdir.parseconfig() + pytester.parseconfig() @pytest.mark.parametrize("block_it", [True, False]) -def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block_it): +def test_plugin_preparse_prevents_setuptools_loading( + pytester: Pytester, monkeypatch: MonkeyPatch, block_it: bool +) -> None: monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) plugin_module_placeholder = object() @@ -992,7 +1026,7 @@ def distributions(): monkeypatch.setattr(importlib_metadata, "distributions", distributions) args = ("-p", "no:mytestplugin") if block_it else () - config = testdir.parseconfig(*args) + config = pytester.parseconfig(*args) config.pluginmanager.import_plugin("mytestplugin") if block_it: assert "mytestplugin" not in sys.modules @@ -1006,7 +1040,12 @@ def distributions(): @pytest.mark.parametrize( "parse_args,should_load", [(("-p", "mytestplugin"), True), ((), False)] ) -def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load): +def test_disable_plugin_autoload( + pytester: Pytester, + monkeypatch: MonkeyPatch, + parse_args: Union[Tuple[str, str], Tuple[()]], + should_load: bool, +) -> None: class DummyEntryPoint: project_name = name = "mytestplugin" group = "pytest11" @@ -1035,8 +1074,8 @@ def distributions(): monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") monkeypatch.setattr(importlib_metadata, "distributions", distributions) - monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) - config = testdir.parseconfig(*parse_args) + monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) # type: ignore[misc] + config = pytester.parseconfig(*parse_args) has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None assert has_loaded == should_load if should_load: @@ -1045,9 +1084,9 @@ def distributions(): assert PseudoPlugin.attrs_used == [] -def test_plugin_loading_order(testdir): +def test_plugin_loading_order(pytester: Pytester) -> None: """Test order of plugin loading with `-p`.""" - p1 = testdir.makepyfile( + p1 = pytester.makepyfile( """ def test_terminal_plugin(request): import myplugin @@ -1066,37 +1105,37 @@ def pytest_sessionstart(session): """ }, ) - testdir.syspathinsert() - result = testdir.runpytest("-p", "myplugin", str(p1)) + pytester.syspathinsert() + result = pytester.runpytest("-p", "myplugin", str(p1)) assert result.ret == 0 -def test_cmdline_processargs_simple(testdir): - testdir.makeconftest( +def test_cmdline_processargs_simple(pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_cmdline_preparse(args): args.append("-h") """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*pytest*", "*-h*"]) -def test_invalid_options_show_extra_information(testdir): +def test_invalid_options_show_extra_information(pytester: Pytester) -> None: """Display extra information when pytest exits due to unrecognized options in the command-line.""" - testdir.makeini( + pytester.makeini( """ [pytest] addopts = --invalid-option """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stderr.fnmatch_lines( [ "*error: unrecognized arguments: --invalid-option*", - "* inifile: %s*" % testdir.tmpdir.join("tox.ini"), - "* rootdir: %s*" % testdir.tmpdir, + "* inifile: %s*" % pytester.path.joinpath("tox.ini"), + "* rootdir: %s*" % pytester.path, ] ) @@ -1110,42 +1149,49 @@ def test_invalid_options_show_extra_information(testdir): ["-v", "dir2", "dir1"], ], ) -def test_consider_args_after_options_for_rootdir(testdir, args): +def test_consider_args_after_options_for_rootdir( + pytester: Pytester, args: List[str] +) -> None: """ Consider all arguments in the command-line for rootdir discovery, even if they happen to occur after an option. #949 """ # replace "dir1" and "dir2" from "args" into their real directory - root = testdir.tmpdir.mkdir("myroot") - d1 = root.mkdir("dir1") - d2 = root.mkdir("dir2") + root = pytester.mkdir("myroot") + d1 = root.joinpath("dir1") + d1.mkdir() + d2 = root.joinpath("dir2") + d2.mkdir() for i, arg in enumerate(args): if arg == "dir1": - args[i] = d1 + args[i] = str(d1) elif arg == "dir2": - args[i] = d2 - with root.as_cwd(): - result = testdir.runpytest(*args) + args[i] = str(d2) + with MonkeyPatch.context() as mp: + mp.chdir(root) + result = pytester.runpytest(*args) result.stdout.fnmatch_lines(["*rootdir: *myroot"]) -def test_toolongargs_issue224(testdir): - result = testdir.runpytest("-m", "hello" * 500) +def test_toolongargs_issue224(pytester: Pytester) -> None: + result = pytester.runpytest("-m", "hello" * 500) assert result.ret == ExitCode.NO_TESTS_COLLECTED -def test_config_in_subdirectory_colon_command_line_issue2148(testdir): +def test_config_in_subdirectory_colon_command_line_issue2148( + pytester: Pytester, +) -> None: conftest_source = """ def pytest_addoption(parser): parser.addini('foo', 'foo') """ - testdir.makefile( + pytester.makefile( ".ini", **{"pytest": "[pytest]\nfoo = root", "subdir/pytest": "[pytest]\nfoo = subdir"}, ) - testdir.makepyfile( + pytester.makepyfile( **{ "conftest": conftest_source, "subdir/conftest": conftest_source, @@ -1156,12 +1202,12 @@ def test_foo(pytestconfig): } ) - result = testdir.runpytest("subdir/test_foo.py::test_foo") + result = pytester.runpytest("subdir/test_foo.py::test_foo") assert result.ret == 0 -def test_notify_exception(testdir, capfd): - config = testdir.parseconfig() +def test_notify_exception(pytester: Pytester, capfd) -> None: + config = pytester.parseconfig() with pytest.raises(ValueError) as excinfo: raise ValueError(1) config.notify_exception(excinfo, config.option) @@ -1177,7 +1223,7 @@ def pytest_internalerror(self): _, err = capfd.readouterr() assert not err - config = testdir.parseconfig("-p", "no:terminal") + config = pytester.parseconfig("-p", "no:terminal") with pytest.raises(ValueError) as excinfo: raise ValueError(1) config.notify_exception(excinfo, config.option) @@ -1185,9 +1231,9 @@ def pytest_internalerror(self): assert "ValueError" in err -def test_no_terminal_discovery_error(testdir): - testdir.makepyfile("raise TypeError('oops!')") - result = testdir.runpytest("-p", "no:terminal", "--collect-only") +def test_no_terminal_discovery_error(pytester: Pytester) -> None: + pytester.makepyfile("raise TypeError('oops!')") + result = pytester.runpytest("-p", "no:terminal", "--collect-only") assert result.ret == ExitCode.INTERRUPTED @@ -1226,10 +1272,10 @@ def exp_match(val: object) -> str: assert _get_plugin_specs_as_list(("foo", "bar")) == ["foo", "bar"] -def test_collect_pytest_prefix_bug_integration(testdir): +def test_collect_pytest_prefix_bug_integration(pytester: Pytester) -> None: """Integration test for issue #3775""" - p = testdir.copy_example("config/collect_pytest_prefix") - result = testdir.runpytest(p) + p = pytester.copy_example("config/collect_pytest_prefix") + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["* 1 passed *"]) @@ -1396,9 +1442,9 @@ def test_with_config_also_in_parent_directory( class TestOverrideIniArgs: @pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split()) - def test_override_ini_names(self, testdir, name): + def test_override_ini_names(self, pytester: Pytester, name: str) -> None: section = "[pytest]" if name != "setup.cfg" else "[tool:pytest]" - testdir.tmpdir.join(name).write( + pytester.path.joinpath(name).write_text( textwrap.dedent( """ {section} @@ -1407,40 +1453,40 @@ def test_override_ini_names(self, testdir, name): ) ) ) - testdir.makeconftest( + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("custom", "")""" ) - testdir.makepyfile( + pytester.makepyfile( """ def test_pass(pytestconfig): ini_val = pytestconfig.getini("custom") print('\\ncustom_option:%s\\n' % ini_val)""" ) - result = testdir.runpytest("--override-ini", "custom=2.0", "-s") + result = pytester.runpytest("--override-ini", "custom=2.0", "-s") assert result.ret == 0 result.stdout.fnmatch_lines(["custom_option:2.0"]) - result = testdir.runpytest( + result = pytester.runpytest( "--override-ini", "custom=2.0", "--override-ini=custom=3.0", "-s" ) assert result.ret == 0 result.stdout.fnmatch_lines(["custom_option:3.0"]) - def test_override_ini_pathlist(self, testdir): - testdir.makeconftest( + def test_override_ini_pathlist(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): parser.addini("paths", "my new ini value", type="pathlist")""" ) - testdir.makeini( + pytester.makeini( """ [pytest] paths=blah.py""" ) - testdir.makepyfile( + pytester.makepyfile( """ import py.path def test_pathlist(pytestconfig): @@ -1449,13 +1495,13 @@ def test_pathlist(pytestconfig): for cpf in config_paths: print('\\nuser_path:%s' % cpf.basename)""" ) - result = testdir.runpytest( + result = pytester.runpytest( "--override-ini", "paths=foo/bar1.py foo/bar2.py", "-s" ) result.stdout.fnmatch_lines(["user_path:bar1.py", "user_path:bar2.py"]) - def test_override_multiple_and_default(self, testdir): - testdir.makeconftest( + def test_override_multiple_and_default(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_addoption(parser): addini = parser.addini @@ -1464,14 +1510,14 @@ def pytest_addoption(parser): addini("custom_option_3", "", default=False, type="bool") addini("custom_option_4", "", default=True, type="bool")""" ) - testdir.makeini( + pytester.makeini( """ [pytest] custom_option_1=custom_option_1 custom_option_2=custom_option_2 """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_multiple_options(pytestconfig): prefix = "custom_option" @@ -1480,7 +1526,7 @@ def test_multiple_options(pytestconfig): print('\\nini%d:%s' % (x, ini_value)) """ ) - result = testdir.runpytest( + result = pytester.runpytest( "--override-ini", "custom_option_1=fulldir=/tmp/user1", "-o", @@ -1500,14 +1546,14 @@ def test_multiple_options(pytestconfig): ] ) - def test_override_ini_usage_error_bad_style(self, testdir): - testdir.makeini( + def test_override_ini_usage_error_bad_style(self, pytester: Pytester) -> None: + pytester.makeini( """ [pytest] xdist_strict=False """ ) - result = testdir.runpytest("--override-ini", "xdist_strict", "True") + result = pytester.runpytest("--override-ini", "xdist_strict", "True") result.stderr.fnmatch_lines( [ "ERROR: -o/--override-ini expects option=value style (got: 'xdist_strict').", @@ -1515,32 +1561,38 @@ def test_override_ini_usage_error_bad_style(self, testdir): ) @pytest.mark.parametrize("with_ini", [True, False]) - def test_override_ini_handled_asap(self, testdir, with_ini): + def test_override_ini_handled_asap( + self, pytester: Pytester, with_ini: bool + ) -> None: """-o should be handled as soon as possible and always override what's in ini files (#2238)""" if with_ini: - testdir.makeini( + pytester.makeini( """ [pytest] python_files=test_*.py """ ) - testdir.makepyfile( + pytester.makepyfile( unittest_ini_handle=""" def test(): pass """ ) - result = testdir.runpytest("--override-ini", "python_files=unittest_*.py") + result = pytester.runpytest("--override-ini", "python_files=unittest_*.py") result.stdout.fnmatch_lines(["*1 passed in*"]) - def test_addopts_before_initini(self, monkeypatch, _config_for_test, _sys_snapshot): + def test_addopts_before_initini( + self, monkeypatch: MonkeyPatch, _config_for_test, _sys_snapshot + ) -> None: cache_dir = ".custom_cache" monkeypatch.setenv("PYTEST_ADDOPTS", "-o cache_dir=%s" % cache_dir) config = _config_for_test config._preparse([], addopts=True) assert config._override_ini == ["cache_dir=%s" % cache_dir] - def test_addopts_from_env_not_concatenated(self, monkeypatch, _config_for_test): + def test_addopts_from_env_not_concatenated( + self, monkeypatch: MonkeyPatch, _config_for_test + ) -> None: """PYTEST_ADDOPTS should not take values from normal args (#4265).""" monkeypatch.setenv("PYTEST_ADDOPTS", "-o") config = _config_for_test @@ -1551,32 +1603,34 @@ def test_addopts_from_env_not_concatenated(self, monkeypatch, _config_for_test): in excinfo.value.args[0] ) - def test_addopts_from_ini_not_concatenated(self, testdir): + def test_addopts_from_ini_not_concatenated(self, pytester: Pytester) -> None: """`addopts` from ini should not take values from normal args (#4265).""" - testdir.makeini( + pytester.makeini( """ [pytest] addopts=-o """ ) - result = testdir.runpytest("cache_dir=ignored") + result = pytester.runpytest("cache_dir=ignored") result.stderr.fnmatch_lines( [ "%s: error: argument -o/--override-ini: expected one argument (via addopts config)" - % (testdir.request.config._parser.optparser.prog,) + % (pytester._request.config._parser.optparser.prog,) ] ) assert result.ret == _pytest.config.ExitCode.USAGE_ERROR - def test_override_ini_does_not_contain_paths(self, _config_for_test, _sys_snapshot): + def test_override_ini_does_not_contain_paths( + self, _config_for_test, _sys_snapshot + ) -> None: """Check that -o no longer swallows all options after it (#3103)""" config = _config_for_test config._preparse(["-o", "cache_dir=/cache", "/some/test/path"]) assert config._override_ini == ["cache_dir=/cache"] - def test_multiple_override_ini_options(self, testdir): + def test_multiple_override_ini_options(self, pytester: Pytester) -> None: """Ensure a file path following a '-o' option does not generate an error (#3103)""" - testdir.makepyfile( + pytester.makepyfile( **{ "conftest.py": """ def pytest_addoption(parser): @@ -1594,19 +1648,19 @@ def test(): """, } ) - result = testdir.runpytest("-o", "foo=1", "-o", "bar=0", "test_foo.py") + result = pytester.runpytest("-o", "foo=1", "-o", "bar=0", "test_foo.py") assert "ERROR:" not in result.stderr.str() result.stdout.fnmatch_lines(["collected 1 item", "*= 1 passed in *="]) -def test_help_via_addopts(testdir): - testdir.makeini( +def test_help_via_addopts(pytester: Pytester) -> None: + pytester.makeini( """ [pytest] addopts = --unknown-option-should-allow-for-help --help """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.ret == 0 result.stdout.fnmatch_lines( [ @@ -1618,8 +1672,8 @@ def test_help_via_addopts(testdir): ) -def test_help_and_version_after_argument_error(testdir): - testdir.makeconftest( +def test_help_and_version_after_argument_error(pytester: Pytester) -> None: + pytester.makeconftest( """ def validate(arg): raise argparse.ArgumentTypeError("argerror") @@ -1632,13 +1686,13 @@ def pytest_addoption(parser): ) """ ) - testdir.makeini( + pytester.makeini( """ [pytest] addopts = --invalid-option-should-allow-for-help """ ) - result = testdir.runpytest("--help") + result = pytester.runpytest("--help") result.stdout.fnmatch_lines( [ "usage: *", @@ -1650,19 +1704,19 @@ def pytest_addoption(parser): [ "ERROR: usage: *", "%s: error: argument --invalid-option-should-allow-for-help: expected one argument" - % (testdir.request.config._parser.optparser.prog,), + % (pytester._request.config._parser.optparser.prog,), ] ) # Does not display full/default help. assert "to see available markers type: pytest --markers" not in result.stdout.lines assert result.ret == ExitCode.USAGE_ERROR - result = testdir.runpytest("--version") + result = pytester.runpytest("--version") result.stderr.fnmatch_lines([f"pytest {pytest.__version__}"]) assert result.ret == ExitCode.USAGE_ERROR -def test_help_formatter_uses_py_get_terminal_width(monkeypatch): +def test_help_formatter_uses_py_get_terminal_width(monkeypatch: MonkeyPatch) -> None: from _pytest.config.argparsing import DropShorterLongHelpFormatter monkeypatch.setenv("COLUMNS", "90") @@ -1677,39 +1731,39 @@ def test_help_formatter_uses_py_get_terminal_width(monkeypatch): assert formatter._width == 42 -def test_config_does_not_load_blocked_plugin_from_args(testdir): +def test_config_does_not_load_blocked_plugin_from_args(pytester: Pytester) -> None: """This tests that pytest's config setup handles "-p no:X".""" - p = testdir.makepyfile("def test(capfd): pass") - result = testdir.runpytest(str(p), "-pno:capture") + p = pytester.makepyfile("def test(capfd): pass") + result = pytester.runpytest(str(p), "-pno:capture") result.stdout.fnmatch_lines(["E fixture 'capfd' not found"]) assert result.ret == ExitCode.TESTS_FAILED - result = testdir.runpytest(str(p), "-pno:capture", "-s") + result = pytester.runpytest(str(p), "-pno:capture", "-s") result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"]) assert result.ret == ExitCode.USAGE_ERROR -def test_invocation_args(testdir): +def test_invocation_args(pytester: Pytester) -> None: """Ensure that Config.invocation_* arguments are correctly defined""" class DummyPlugin: pass - p = testdir.makepyfile("def test(): pass") + p = pytester.makepyfile("def test(): pass") plugin = DummyPlugin() - rec = testdir.inline_run(p, "-v", plugins=[plugin]) + rec = pytester.inline_run(p, "-v", plugins=[plugin]) calls = rec.getcalls("pytest_runtest_protocol") assert len(calls) == 1 call = calls[0] config = call.item.config - assert config.invocation_params.args == (p, "-v") - assert config.invocation_params.dir == Path(str(testdir.tmpdir)) + assert config.invocation_params.args == (str(p), "-v") + assert config.invocation_params.dir == pytester.path plugins = config.invocation_params.plugins assert len(plugins) == 2 assert plugins[0] is plugin - assert type(plugins[1]).__name__ == "Collect" # installed by testdir.inline_run() + assert type(plugins[1]).__name__ == "Collect" # installed by pytester.inline_run() # args cannot be None with pytest.raises(TypeError): @@ -1724,7 +1778,7 @@ class DummyPlugin: if x not in _pytest.config.essential_plugins ], ) -def test_config_blocked_default_plugins(testdir, plugin): +def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None: if plugin == "debugging": # Fixed in xdist master (after 1.27.0). # https://github.com/pytest-dev/pytest-xdist/pull/422 @@ -1735,8 +1789,8 @@ def test_config_blocked_default_plugins(testdir, plugin): else: pytest.skip("does not work with xdist currently") - p = testdir.makepyfile("def test(): pass") - result = testdir.runpytest(str(p), "-pno:%s" % plugin) + p = pytester.makepyfile("def test(): pass") + result = pytester.runpytest(str(p), "-pno:%s" % plugin) if plugin == "python": assert result.ret == ExitCode.USAGE_ERROR @@ -1752,8 +1806,8 @@ def test_config_blocked_default_plugins(testdir, plugin): if plugin != "terminal": result.stdout.fnmatch_lines(["* 1 passed in *"]) - p = testdir.makepyfile("def test(): assert 0") - result = testdir.runpytest(str(p), "-pno:%s" % plugin) + p = pytester.makepyfile("def test(): assert 0") + result = pytester.runpytest(str(p), "-pno:%s" % plugin) assert result.ret == ExitCode.TESTS_FAILED if plugin != "terminal": result.stdout.fnmatch_lines(["* 1 failed in *"]) @@ -1762,8 +1816,8 @@ def test_config_blocked_default_plugins(testdir, plugin): class TestSetupCfg: - def test_pytest_setup_cfg_unsupported(self, testdir): - testdir.makefile( + def test_pytest_setup_cfg_unsupported(self, pytester: Pytester) -> None: + pytester.makefile( ".cfg", setup=""" [pytest] @@ -1771,10 +1825,10 @@ def test_pytest_setup_cfg_unsupported(self, testdir): """, ) with pytest.raises(pytest.fail.Exception): - testdir.runpytest() + pytester.runpytest() - def test_pytest_custom_cfg_unsupported(self, testdir): - testdir.makefile( + def test_pytest_custom_cfg_unsupported(self, pytester: Pytester) -> None: + pytester.makefile( ".cfg", custom=""" [pytest] @@ -1782,33 +1836,35 @@ def test_pytest_custom_cfg_unsupported(self, testdir): """, ) with pytest.raises(pytest.fail.Exception): - testdir.runpytest("-c", "custom.cfg") + pytester.runpytest("-c", "custom.cfg") class TestPytestPluginsVariable: - def test_pytest_plugins_in_non_top_level_conftest_unsupported(self, testdir): - testdir.makepyfile( + def test_pytest_plugins_in_non_top_level_conftest_unsupported( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( **{ "subdirectory/conftest.py": """ pytest_plugins=['capture'] """ } ) - testdir.makepyfile( + pytester.makepyfile( """ def test_func(): pass """ ) - res = testdir.runpytest() + res = pytester.runpytest() assert res.ret == 2 msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" res.stdout.fnmatch_lines([f"*{msg}*", f"*subdirectory{os.sep}conftest.py*"]) @pytest.mark.parametrize("use_pyargs", [True, False]) def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( - self, testdir, use_pyargs - ): + self, pytester: Pytester, use_pyargs: bool + ) -> None: """When using --pyargs, do not emit the warning about non-top-level conftest warnings (#4039, #4044)""" files = { @@ -1819,11 +1875,11 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( "src/pkg/sub/conftest.py": "pytest_plugins=['capture']", "src/pkg/sub/test_bar.py": "def test(): pass", } - testdir.makepyfile(**files) - testdir.syspathinsert(testdir.tmpdir.join("src")) + pytester.makepyfile(**files) + pytester.syspathinsert(pytester.path.joinpath("src")) args = ("--pyargs", "pkg") if use_pyargs else () - res = testdir.runpytest(*args) + res = pytester.runpytest(*args) assert res.ret == (0 if use_pyargs else 2) msg = ( msg @@ -1834,33 +1890,35 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( res.stdout.fnmatch_lines([f"*{msg}*"]) def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest( - self, testdir - ): - subdirectory = testdir.tmpdir.join("subdirectory") + self, pytester: Pytester + ) -> None: + subdirectory = pytester.path.joinpath("subdirectory") subdirectory.mkdir() - testdir.makeconftest( + pytester.makeconftest( """ pytest_plugins=['capture'] """ ) - testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py")) + pytester.path.joinpath("conftest.py").rename( + subdirectory.joinpath("conftest.py") + ) - testdir.makepyfile( + pytester.makepyfile( """ def test_func(): pass """ ) - res = testdir.runpytest_subprocess() + res = pytester.runpytest_subprocess() assert res.ret == 2 msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" res.stdout.fnmatch_lines([f"*{msg}*", f"*subdirectory{os.sep}conftest.py*"]) def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives( - self, testdir - ): - testdir.makepyfile( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( "def test_func(): pass", **{ "subdirectory/conftest": "pass", @@ -1871,13 +1929,13 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives """, }, ) - res = testdir.runpytest_subprocess() + res = pytester.runpytest_subprocess() assert res.ret == 0 msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" assert msg not in res.stdout.str() -def test_conftest_import_error_repr(tmpdir): +def test_conftest_import_error_repr(tmpdir: py.path.local) -> None: """`ConftestImportFailure` should use a short error message and readable path to the failed conftest.py file.""" path = tmpdir.join("foo/conftest.py") @@ -1893,7 +1951,7 @@ def test_conftest_import_error_repr(tmpdir): raise ConftestImportFailure(path, exc_info) from exc -def test_strtobool(): +def test_strtobool() -> None: assert _strtobool("YES") assert not _strtobool("NO") with pytest.raises(ValueError): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 0b861f25a7b..a4d22d2aa3b 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -20,7 +20,7 @@ from _pytest.config import Config from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester from _pytest.reports import BaseReport from _pytest.reports import CollectReport from _pytest.terminal import _folded_skips @@ -76,8 +76,8 @@ def test_plugin_nameversion(input, expected): class TestTerminal: - def test_pass_skip_fail(self, testdir, option): - testdir.makepyfile( + def test_pass_skip_fail(self, pytester: Pytester, option) -> None: + pytester.makepyfile( """ import pytest def test_ok(): @@ -88,7 +88,7 @@ def test_func(): assert 0 """ ) - result = testdir.runpytest(*option.args) + result = pytester.runpytest(*option.args) if option.verbosity > 0: result.stdout.fnmatch_lines( [ @@ -105,16 +105,16 @@ def test_func(): [" def test_func():", "> assert 0", "E assert 0"] ) - def test_internalerror(self, testdir, linecomp): - modcol = testdir.getmodulecol("def test_one(): pass") + def test_internalerror(self, pytester: Pytester, linecomp) -> None: + modcol = pytester.getmodulecol("def test_one(): pass") rep = TerminalReporter(modcol.config, file=linecomp.stringio) with pytest.raises(ValueError) as excinfo: raise ValueError("hello") rep.pytest_internalerror(excinfo.getrepr()) linecomp.assert_contains_lines(["INTERNALERROR> *ValueError*hello*"]) - def test_writeline(self, testdir, linecomp): - modcol = testdir.getmodulecol("def test_one(): pass") + def test_writeline(self, pytester: Pytester, linecomp) -> None: + modcol = pytester.getmodulecol("def test_one(): pass") rep = TerminalReporter(modcol.config, file=linecomp.stringio) rep.write_fspath_result(modcol.nodeid, ".") rep.write_line("hello world") @@ -123,8 +123,8 @@ def test_writeline(self, testdir, linecomp): assert lines[1].endswith(modcol.name + " .") assert lines[2] == "hello world" - def test_show_runtest_logstart(self, testdir, linecomp): - item = testdir.getitem("def test_func(): pass") + def test_show_runtest_logstart(self, pytester: Pytester, linecomp) -> None: + item = pytester.getitem("def test_func(): pass") tr = TerminalReporter(item.config, file=linecomp.stringio) item.config.pluginmanager.register(tr) location = item.reportinfo() @@ -133,7 +133,9 @@ def test_show_runtest_logstart(self, testdir, linecomp): ) linecomp.assert_contains_lines(["*test_show_runtest_logstart.py*"]) - def test_runtest_location_shown_before_test_starts(self, pytester): + def test_runtest_location_shown_before_test_starts( + self, pytester: Pytester + ) -> None: pytester.makepyfile( """ def test_1(): @@ -146,7 +148,9 @@ def test_1(): child.sendeof() child.kill(15) - def test_report_collect_after_half_a_second(self, pytester, monkeypatch): + def test_report_collect_after_half_a_second( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: """Test for "collecting" being updated after 0.5s""" pytester.makepyfile( @@ -173,8 +177,10 @@ def test_1(): rest = child.read().decode("utf8") assert "= \x1b[32m\x1b[1m2 passed\x1b[0m\x1b[32m in" in rest - def test_itemreport_subclasses_show_subclassed_file(self, testdir): - testdir.makepyfile( + def test_itemreport_subclasses_show_subclassed_file( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( **{ "tests/test_p1": """ class BaseTests(object): @@ -197,10 +203,10 @@ class TestMore(BaseTests): pass """, } ) - result = testdir.runpytest("tests/test_p2.py", "--rootdir=tests") + result = pytester.runpytest("tests/test_p2.py", "--rootdir=tests") result.stdout.fnmatch_lines(["tests/test_p2.py .*", "=* 1 passed in *"]) - result = testdir.runpytest("-vv", "-rA", "tests/test_p2.py", "--rootdir=tests") + result = pytester.runpytest("-vv", "-rA", "tests/test_p2.py", "--rootdir=tests") result.stdout.fnmatch_lines( [ "tests/test_p2.py::TestMore::test_p1 <- test_p1.py PASSED *", @@ -208,7 +214,7 @@ class TestMore(BaseTests): pass "PASSED tests/test_p2.py::TestMore::test_p1", ] ) - result = testdir.runpytest("-vv", "-rA", "tests/test_p3.py", "--rootdir=tests") + result = pytester.runpytest("-vv", "-rA", "tests/test_p3.py", "--rootdir=tests") result.stdout.fnmatch_lines( [ "tests/test_p3.py::TestMore::test_p1 <- test_p1.py FAILED *", @@ -224,9 +230,11 @@ class TestMore(BaseTests): pass ] ) - def test_itemreport_directclasses_not_shown_as_subclasses(self, testdir): - a = testdir.mkpydir("a123") - a.join("test_hello123.py").write( + def test_itemreport_directclasses_not_shown_as_subclasses( + self, pytester: Pytester + ) -> None: + a = pytester.mkpydir("a123") + a.joinpath("test_hello123.py").write_text( textwrap.dedent( """\ class TestClass(object): @@ -235,14 +243,14 @@ def test_method(self): """ ) ) - result = testdir.runpytest("-vv") + result = pytester.runpytest("-vv") assert result.ret == 0 result.stdout.fnmatch_lines(["*a123/test_hello123.py*PASS*"]) result.stdout.no_fnmatch_line("* <- *") @pytest.mark.parametrize("fulltrace", ("", "--fulltrace")) - def test_keyboard_interrupt(self, testdir, fulltrace): - testdir.makepyfile( + def test_keyboard_interrupt(self, pytester: Pytester, fulltrace) -> None: + pytester.makepyfile( """ def test_foobar(): assert 0 @@ -253,7 +261,7 @@ def test_interrupt_me(): """ ) - result = testdir.runpytest(fulltrace, no_reraise_ctrlc=True) + result = pytester.runpytest(fulltrace, no_reraise_ctrlc=True) result.stdout.fnmatch_lines( [ " def test_foobar():", @@ -272,37 +280,37 @@ def test_interrupt_me(): ) result.stdout.fnmatch_lines(["*KeyboardInterrupt*"]) - def test_keyboard_in_sessionstart(self, testdir): - testdir.makeconftest( + def test_keyboard_in_sessionstart(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_sessionstart(): raise KeyboardInterrupt """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_foobar(): pass """ ) - result = testdir.runpytest(no_reraise_ctrlc=True) + result = pytester.runpytest(no_reraise_ctrlc=True) assert result.ret == 2 result.stdout.fnmatch_lines(["*KeyboardInterrupt*"]) - def test_collect_single_item(self, testdir): + def test_collect_single_item(self, pytester: Pytester) -> None: """Use singular 'item' when reporting a single test item""" - testdir.makepyfile( + pytester.makepyfile( """ def test_foobar(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["collected 1 item"]) - def test_rewrite(self, testdir, monkeypatch): - config = testdir.parseconfig() + def test_rewrite(self, pytester: Pytester, monkeypatch) -> None: + config = pytester.parseconfig() f = StringIO() monkeypatch.setattr(f, "isatty", lambda *args: True) tr = TerminalReporter(config, f) @@ -312,57 +320,57 @@ def test_rewrite(self, testdir, monkeypatch): assert f.getvalue() == "hello" + "\r" + "hey" + (6 * " ") def test_report_teststatus_explicit_markup( - self, testdir: Testdir, color_mapping + self, monkeypatch: MonkeyPatch, pytester: Pytester, color_mapping ) -> None: """Test that TerminalReporter handles markup explicitly provided by a pytest_report_teststatus hook.""" - testdir.monkeypatch.setenv("PY_COLORS", "1") - testdir.makeconftest( + monkeypatch.setenv("PY_COLORS", "1") + pytester.makeconftest( """ def pytest_report_teststatus(report): return 'foo', 'F', ('FOO', {'red': True}) """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_foobar(): pass """ ) - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch(["*{red}FOO{reset}*"]) ) class TestCollectonly: - def test_collectonly_basic(self, testdir): - testdir.makepyfile( + def test_collectonly_basic(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_func(): pass """ ) - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines( ["", " "] ) - def test_collectonly_skipped_module(self, testdir): - testdir.makepyfile( + def test_collectonly_skipped_module(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest pytest.skip("hello") """ ) - result = testdir.runpytest("--collect-only", "-rs") + result = pytester.runpytest("--collect-only", "-rs") result.stdout.fnmatch_lines(["*ERROR collecting*"]) def test_collectonly_displays_test_description( - self, testdir: Testdir, dummy_yaml_custom_test + self, pytester: Pytester, dummy_yaml_custom_test ) -> None: """Used dummy_yaml_custom_test for an Item without ``obj``.""" - testdir.makepyfile( + pytester.makepyfile( """ def test_with_description(): ''' This test has a description. @@ -371,7 +379,7 @@ def test_with_description(): more2.''' """ ) - result = testdir.runpytest("--collect-only", "--verbose") + result = pytester.runpytest("--collect-only", "--verbose") result.stdout.fnmatch_lines( [ "", @@ -386,24 +394,24 @@ def test_with_description(): consecutive=True, ) - def test_collectonly_failed_module(self, testdir): - testdir.makepyfile("""raise ValueError(0)""") - result = testdir.runpytest("--collect-only") + def test_collectonly_failed_module(self, pytester: Pytester) -> None: + pytester.makepyfile("""raise ValueError(0)""") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["*raise ValueError*", "*1 error*"]) - def test_collectonly_fatal(self, testdir): - testdir.makeconftest( + def test_collectonly_fatal(self, pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_collectstart(collector): assert 0, "urgs" """ ) - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["*INTERNAL*args*"]) assert result.ret == 3 - def test_collectonly_simple(self, testdir): - p = testdir.makepyfile( + def test_collectonly_simple(self, pytester: Pytester) -> None: + p = pytester.makepyfile( """ def test_func1(): pass @@ -412,7 +420,7 @@ def test_method(self): pass """ ) - result = testdir.runpytest("--collect-only", p) + result = pytester.runpytest("--collect-only", p) # assert stderr.startswith("inserting into sys.path") assert result.ret == 0 result.stdout.fnmatch_lines( @@ -424,9 +432,9 @@ def test_method(self): ] ) - def test_collectonly_error(self, testdir): - p = testdir.makepyfile("import Errlkjqweqwe") - result = testdir.runpytest("--collect-only", p) + def test_collectonly_error(self, pytester: Pytester) -> None: + p = pytester.makepyfile("import Errlkjqweqwe") + result = pytester.runpytest("--collect-only", p) assert result.ret == 2 result.stdout.fnmatch_lines( textwrap.dedent( @@ -439,28 +447,28 @@ def test_collectonly_error(self, testdir): ).strip() ) - def test_collectonly_missing_path(self, testdir): + def test_collectonly_missing_path(self, pytester: Pytester) -> None: """Issue 115: failure in parseargs will cause session not to have the items attribute.""" - result = testdir.runpytest("--collect-only", "uhm_missing_path") + result = pytester.runpytest("--collect-only", "uhm_missing_path") assert result.ret == 4 result.stderr.fnmatch_lines( ["*ERROR: file or directory not found: uhm_missing_path"] ) - def test_collectonly_quiet(self, testdir): - testdir.makepyfile("def test_foo(): pass") - result = testdir.runpytest("--collect-only", "-q") + def test_collectonly_quiet(self, pytester: Pytester) -> None: + pytester.makepyfile("def test_foo(): pass") + result = pytester.runpytest("--collect-only", "-q") result.stdout.fnmatch_lines(["*test_foo*"]) - def test_collectonly_more_quiet(self, testdir): - testdir.makepyfile(test_fun="def test_foo(): pass") - result = testdir.runpytest("--collect-only", "-qq") + def test_collectonly_more_quiet(self, pytester: Pytester) -> None: + pytester.makepyfile(test_fun="def test_foo(): pass") + result = pytester.runpytest("--collect-only", "-qq") result.stdout.fnmatch_lines(["*test_fun.py: 1*"]) - def test_collect_only_summary_status(self, testdir): + def test_collect_only_summary_status(self, pytester: Pytester) -> None: """Custom status depending on test selection using -k or -m. #7701.""" - testdir.makepyfile( + pytester.makepyfile( test_collect_foo=""" def test_foo(): pass """, @@ -469,41 +477,41 @@ def test_foobar(): pass def test_bar(): pass """, ) - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines("*== 3 tests collected in * ==*") - result = testdir.runpytest("--collect-only", "test_collect_foo.py") + result = pytester.runpytest("--collect-only", "test_collect_foo.py") result.stdout.fnmatch_lines("*== 1 test collected in * ==*") - result = testdir.runpytest("--collect-only", "-k", "foo") + result = pytester.runpytest("--collect-only", "-k", "foo") result.stdout.fnmatch_lines("*== 2/3 tests collected (1 deselected) in * ==*") - result = testdir.runpytest("--collect-only", "-k", "test_bar") + result = pytester.runpytest("--collect-only", "-k", "test_bar") result.stdout.fnmatch_lines("*== 1/3 tests collected (2 deselected) in * ==*") - result = testdir.runpytest("--collect-only", "-k", "invalid") + result = pytester.runpytest("--collect-only", "-k", "invalid") result.stdout.fnmatch_lines("*== no tests collected (3 deselected) in * ==*") - testdir.mkdir("no_tests_here") - result = testdir.runpytest("--collect-only", "no_tests_here") + pytester.mkdir("no_tests_here") + result = pytester.runpytest("--collect-only", "no_tests_here") result.stdout.fnmatch_lines("*== no tests collected in * ==*") - testdir.makepyfile( + pytester.makepyfile( test_contains_error=""" raise RuntimeError """, ) - result = testdir.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines("*== 3 tests collected, 1 error in * ==*") - result = testdir.runpytest("--collect-only", "-k", "foo") + result = pytester.runpytest("--collect-only", "-k", "foo") result.stdout.fnmatch_lines( "*== 2/3 tests collected (1 deselected), 1 error in * ==*" ) class TestFixtureReporting: - def test_setup_fixture_error(self, testdir): - testdir.makepyfile( + def test_setup_fixture_error(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def setup_function(function): print("setup func") @@ -512,7 +520,7 @@ def test_nada(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*ERROR at setup of test_nada*", @@ -524,8 +532,8 @@ def test_nada(): ) assert result.ret != 0 - def test_teardown_fixture_error(self, testdir): - testdir.makepyfile( + def test_teardown_fixture_error(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_nada(): pass @@ -534,7 +542,7 @@ def teardown_function(function): assert 0 """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*ERROR at teardown*", @@ -546,8 +554,8 @@ def teardown_function(function): ] ) - def test_teardown_fixture_error_and_test_failure(self, testdir): - testdir.makepyfile( + def test_teardown_fixture_error_and_test_failure(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test_fail(): assert 0, "failingfunc" @@ -557,7 +565,7 @@ def teardown_function(function): assert False """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*ERROR at teardown of test_fail*", @@ -572,9 +580,9 @@ def teardown_function(function): ] ) - def test_setup_teardown_output_and_test_failure(self, testdir): + def test_setup_teardown_output_and_test_failure(self, pytester: Pytester) -> None: """Test for issue #442.""" - testdir.makepyfile( + pytester.makepyfile( """ def setup_function(function): print("setup func") @@ -586,7 +594,7 @@ def teardown_function(function): print("teardown func") """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*test_fail*", @@ -602,8 +610,8 @@ def teardown_function(function): class TestTerminalFunctional: - def test_deselected(self, testdir): - testpath = testdir.makepyfile( + def test_deselected(self, pytester: Pytester) -> None: + testpath = pytester.makepyfile( """ def test_one(): pass @@ -613,14 +621,14 @@ def test_three(): pass """ ) - result = testdir.runpytest("-k", "test_two:", testpath) + result = pytester.runpytest("-k", "test_two:", testpath) result.stdout.fnmatch_lines( ["collected 3 items / 1 deselected / 2 selected", "*test_deselected.py ..*"] ) assert result.ret == 0 - def test_deselected_with_hookwrapper(self, testdir): - testpath = testdir.makeconftest( + def test_deselected_with_hookwrapper(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @@ -631,7 +639,7 @@ def pytest_collection_modifyitems(config, items): config.hook.pytest_deselected(items=[deselected]) """ ) - testpath = testdir.makepyfile( + testpath = pytester.makepyfile( """ def test_one(): pass @@ -641,7 +649,7 @@ def test_three(): pass """ ) - result = testdir.runpytest(testpath) + result = pytester.runpytest(testpath) result.stdout.fnmatch_lines( [ "collected 3 items / 1 deselected / 2 selected", @@ -650,8 +658,10 @@ def test_three(): ) assert result.ret == 0 - def test_show_deselected_items_using_markexpr_before_test_execution(self, testdir): - testdir.makepyfile( + def test_show_deselected_items_using_markexpr_before_test_execution( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( test_show_deselected=""" import pytest @@ -667,7 +677,7 @@ def test_pass(): pass """ ) - result = testdir.runpytest("-m", "not foo") + result = pytester.runpytest("-m", "not foo") result.stdout.fnmatch_lines( [ "collected 3 items / 1 deselected / 2 selected", @@ -678,8 +688,8 @@ def test_pass(): result.stdout.no_fnmatch_line("*= 1 deselected =*") assert result.ret == 0 - def test_no_skip_summary_if_failure(self, testdir): - testdir.makepyfile( + def test_no_skip_summary_if_failure(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest def test_ok(): @@ -690,12 +700,12 @@ def test_skip(): pytest.skip("dontshow") """ ) - result = testdir.runpytest() + result = pytester.runpytest() assert result.stdout.str().find("skip test summary") == -1 assert result.ret == 1 - def test_passes(self, testdir): - p1 = testdir.makepyfile( + def test_passes(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def test_passes(): pass @@ -704,23 +714,26 @@ def test_method(self): pass """ ) - old = p1.dirpath().chdir() + old = p1.parent + pytester.chdir() try: - result = testdir.runpytest() + result = pytester.runpytest() finally: - old.chdir() + os.chdir(old) result.stdout.fnmatch_lines(["test_passes.py ..*", "* 2 pass*"]) assert result.ret == 0 - def test_header_trailer_info(self, testdir, request): - testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") - testdir.makepyfile( + def test_header_trailer_info( + self, monkeypatch: MonkeyPatch, pytester: Pytester, request + ) -> None: + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + pytester.makepyfile( """ def test_passes(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() verinfo = ".".join(map(str, sys.version_info[:3])) result.stdout.fnmatch_lines( [ @@ -740,15 +753,17 @@ def test_passes(): if request.config.pluginmanager.list_plugin_distinfo(): result.stdout.fnmatch_lines(["plugins: *"]) - def test_no_header_trailer_info(self, testdir, request): - testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") - testdir.makepyfile( + def test_no_header_trailer_info( + self, monkeypatch: MonkeyPatch, pytester: Pytester, request + ) -> None: + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + pytester.makepyfile( """ def test_passes(): pass """ ) - result = testdir.runpytest("--no-header") + result = pytester.runpytest("--no-header") verinfo = ".".join(map(str, sys.version_info[:3])) result.stdout.no_fnmatch_line( "platform %s -- Python %s*pytest-%s*py-%s*pluggy-%s" @@ -763,42 +778,42 @@ def test_passes(): if request.config.pluginmanager.list_plugin_distinfo(): result.stdout.no_fnmatch_line("plugins: *") - def test_header(self, testdir): - testdir.tmpdir.join("tests").ensure_dir() - testdir.tmpdir.join("gui").ensure_dir() + def test_header(self, pytester: Pytester) -> None: + pytester.path.joinpath("tests").mkdir() + pytester.path.joinpath("gui").mkdir() # no ini file - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["rootdir: *test_header0"]) # with configfile - testdir.makeini("""[pytest]""") - result = testdir.runpytest() + pytester.makeini("""[pytest]""") + result = pytester.runpytest() result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"]) # with testpaths option, and not passing anything in the command-line - testdir.makeini( + pytester.makeini( """ [pytest] testpaths = tests gui """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( ["rootdir: *test_header0, configfile: tox.ini, testpaths: tests, gui"] ) # with testpaths option, passing directory in command-line: do not show testpaths then - result = testdir.runpytest("tests") + result = pytester.runpytest("tests") result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"]) def test_header_absolute_testpath( - self, testdir: Testdir, monkeypatch: MonkeyPatch + self, pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: """Regresstion test for #7814.""" - tests = testdir.tmpdir.join("tests") - tests.ensure_dir() - testdir.makepyprojecttoml( + tests = pytester.path.joinpath("tests") + tests.mkdir() + pytester.makepyprojecttoml( """ [tool.pytest.ini_options] testpaths = ['{}'] @@ -806,7 +821,7 @@ def test_header_absolute_testpath( tests ) ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "rootdir: *absolute_testpath0, configfile: pyproject.toml, testpaths: {}".format( @@ -815,38 +830,38 @@ def test_header_absolute_testpath( ] ) - def test_no_header(self, testdir): - testdir.tmpdir.join("tests").ensure_dir() - testdir.tmpdir.join("gui").ensure_dir() + def test_no_header(self, pytester: Pytester) -> None: + pytester.path.joinpath("tests").mkdir() + pytester.path.joinpath("gui").mkdir() # with testpaths option, and not passing anything in the command-line - testdir.makeini( + pytester.makeini( """ [pytest] testpaths = tests gui """ ) - result = testdir.runpytest("--no-header") + result = pytester.runpytest("--no-header") result.stdout.no_fnmatch_line( "rootdir: *test_header0, inifile: tox.ini, testpaths: tests, gui" ) # with testpaths option, passing directory in command-line: do not show testpaths then - result = testdir.runpytest("tests", "--no-header") + result = pytester.runpytest("tests", "--no-header") result.stdout.no_fnmatch_line("rootdir: *test_header0, inifile: tox.ini") - def test_no_summary(self, testdir): - p1 = testdir.makepyfile( + def test_no_summary(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def test_no_summary(): assert false """ ) - result = testdir.runpytest(p1, "--no-summary") + result = pytester.runpytest(p1, "--no-summary") result.stdout.no_fnmatch_line("*= FAILURES =*") - def test_showlocals(self, testdir): - p1 = testdir.makepyfile( + def test_showlocals(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def test_showlocals(): x = 3 @@ -854,7 +869,7 @@ def test_showlocals(): assert 0 """ ) - result = testdir.runpytest(p1, "-l") + result = pytester.runpytest(p1, "-l") result.stdout.fnmatch_lines( [ # "_ _ * Locals *", @@ -863,8 +878,8 @@ def test_showlocals(): ] ) - def test_showlocals_short(self, testdir): - p1 = testdir.makepyfile( + def test_showlocals_short(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile( """ def test_showlocals_short(): x = 3 @@ -872,7 +887,7 @@ def test_showlocals_short(): assert 0 """ ) - result = testdir.runpytest(p1, "-l", "--tb=short") + result = pytester.runpytest(p1, "-l", "--tb=short") result.stdout.fnmatch_lines( [ "test_showlocals_short.py:*", @@ -884,8 +899,8 @@ def test_showlocals_short(): ) @pytest.fixture - def verbose_testfile(self, testdir): - return testdir.makepyfile( + def verbose_testfile(self, pytester: Pytester) -> Path: + return pytester.makepyfile( """ import pytest def test_fail(): @@ -902,8 +917,8 @@ def check(x): """ ) - def test_verbose_reporting(self, verbose_testfile, testdir): - result = testdir.runpytest( + def test_verbose_reporting(self, verbose_testfile, pytester: Pytester) -> None: + result = pytester.runpytest( verbose_testfile, "-v", "-Walways::pytest.PytestWarning" ) result.stdout.fnmatch_lines( @@ -916,12 +931,18 @@ def test_verbose_reporting(self, verbose_testfile, testdir): ) assert result.ret == 1 - def test_verbose_reporting_xdist(self, verbose_testfile, testdir, pytestconfig): + def test_verbose_reporting_xdist( + self, + verbose_testfile, + monkeypatch: MonkeyPatch, + pytester: Pytester, + pytestconfig, + ) -> None: if not pytestconfig.pluginmanager.get_plugin("xdist"): pytest.skip("xdist plugin not installed") - testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") - result = testdir.runpytest( + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD") + result = pytester.runpytest( verbose_testfile, "-v", "-n 1", "-Walways::pytest.PytestWarning" ) result.stdout.fnmatch_lines( @@ -929,35 +950,35 @@ def test_verbose_reporting_xdist(self, verbose_testfile, testdir, pytestconfig): ) assert result.ret == 1 - def test_quiet_reporting(self, testdir): - p1 = testdir.makepyfile("def test_pass(): pass") - result = testdir.runpytest(p1, "-q") + def test_quiet_reporting(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile("def test_pass(): pass") + result = pytester.runpytest(p1, "-q") s = result.stdout.str() assert "test session starts" not in s - assert p1.basename not in s + assert p1.name not in s assert "===" not in s assert "passed" in s - def test_more_quiet_reporting(self, testdir): - p1 = testdir.makepyfile("def test_pass(): pass") - result = testdir.runpytest(p1, "-qq") + def test_more_quiet_reporting(self, pytester: Pytester) -> None: + p1 = pytester.makepyfile("def test_pass(): pass") + result = pytester.runpytest(p1, "-qq") s = result.stdout.str() assert "test session starts" not in s - assert p1.basename not in s + assert p1.name not in s assert "===" not in s assert "passed" not in s @pytest.mark.parametrize( "params", [(), ("--collect-only",)], ids=["no-params", "collect-only"] ) - def test_report_collectionfinish_hook(self, testdir, params): - testdir.makeconftest( + def test_report_collectionfinish_hook(self, pytester: Pytester, params) -> None: + pytester.makeconftest( """ def pytest_report_collectionfinish(config, startdir, items): return ['hello from hook: {0} items'.format(len(items))] """ ) - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.mark.parametrize('i', range(3)) @@ -965,25 +986,25 @@ def test(i): pass """ ) - result = testdir.runpytest(*params) + result = pytester.runpytest(*params) result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"]) - def test_summary_f_alias(self, testdir): + def test_summary_f_alias(self, pytester: Pytester) -> None: """Test that 'f' and 'F' report chars are aliases and don't show up twice in the summary (#6334)""" - testdir.makepyfile( + pytester.makepyfile( """ def test(): assert False """ ) - result = testdir.runpytest("-rfF") + result = pytester.runpytest("-rfF") expected = "FAILED test_summary_f_alias.py::test - assert False" result.stdout.fnmatch_lines([expected]) assert result.stdout.lines.count(expected) == 1 - def test_summary_s_alias(self, testdir): + def test_summary_s_alias(self, pytester: Pytester) -> None: """Test that 's' and 'S' report chars are aliases and don't show up twice in the summary""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @@ -992,18 +1013,18 @@ def test(): pass """ ) - result = testdir.runpytest("-rsS") + result = pytester.runpytest("-rsS") expected = "SKIPPED [1] test_summary_s_alias.py:3: unconditional skip" result.stdout.fnmatch_lines([expected]) assert result.stdout.lines.count(expected) == 1 -def test_fail_extra_reporting(testdir, monkeypatch): +def test_fail_extra_reporting(pytester: Pytester, monkeypatch) -> None: monkeypatch.setenv("COLUMNS", "80") - testdir.makepyfile("def test_this(): assert 0, 'this_failed' * 100") - result = testdir.runpytest("-rN") + pytester.makepyfile("def test_this(): assert 0, 'this_failed' * 100") + result = pytester.runpytest("-rN") result.stdout.no_fnmatch_line("*short test summary*") - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( [ "*test summary*", @@ -1012,28 +1033,28 @@ def test_fail_extra_reporting(testdir, monkeypatch): ) -def test_fail_reporting_on_pass(testdir): - testdir.makepyfile("def test_this(): assert 1") - result = testdir.runpytest("-rf") +def test_fail_reporting_on_pass(pytester: Pytester) -> None: + pytester.makepyfile("def test_this(): assert 1") + result = pytester.runpytest("-rf") result.stdout.no_fnmatch_line("*short test summary*") -def test_pass_extra_reporting(testdir): - testdir.makepyfile("def test_this(): assert 1") - result = testdir.runpytest() +def test_pass_extra_reporting(pytester: Pytester) -> None: + pytester.makepyfile("def test_this(): assert 1") + result = pytester.runpytest() result.stdout.no_fnmatch_line("*short test summary*") - result = testdir.runpytest("-rp") + result = pytester.runpytest("-rp") result.stdout.fnmatch_lines(["*test summary*", "PASS*test_pass_extra_reporting*"]) -def test_pass_reporting_on_fail(testdir): - testdir.makepyfile("def test_this(): assert 0") - result = testdir.runpytest("-rp") +def test_pass_reporting_on_fail(pytester: Pytester) -> None: + pytester.makepyfile("def test_this(): assert 0") + result = pytester.runpytest("-rp") result.stdout.no_fnmatch_line("*short test summary*") -def test_pass_output_reporting(testdir): - testdir.makepyfile( +def test_pass_output_reporting(pytester: Pytester) -> None: + pytester.makepyfile( """ def setup_module(): print("setup_module") @@ -1048,12 +1069,12 @@ def test_pass_no_output(): pass """ ) - result = testdir.runpytest() + result = pytester.runpytest() s = result.stdout.str() assert "test_pass_has_output" not in s assert "Four score and seven years ago..." not in s assert "test_pass_no_output" not in s - result = testdir.runpytest("-rPp") + result = pytester.runpytest("-rPp") result.stdout.fnmatch_lines( [ "*= PASSES =*", @@ -1072,8 +1093,8 @@ def test_pass_no_output(): ) -def test_color_yes(testdir, color_mapping): - p1 = testdir.makepyfile( +def test_color_yes(pytester: Pytester, color_mapping) -> None: + p1 = pytester.makepyfile( """ def fail(): assert 0 @@ -1082,7 +1103,7 @@ def test_this(): fail() """ ) - result = testdir.runpytest("--color=yes", str(p1)) + result = pytester.runpytest("--color=yes", str(p1)) result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ @@ -1109,7 +1130,7 @@ def test_this(): ] ) ) - result = testdir.runpytest("--color=yes", "--tb=short", str(p1)) + result = pytester.runpytest("--color=yes", "--tb=short", str(p1)) result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ @@ -1131,17 +1152,17 @@ def test_this(): ) -def test_color_no(testdir): - testdir.makepyfile("def test_this(): assert 1") - result = testdir.runpytest("--color=no") +def test_color_no(pytester: Pytester) -> None: + pytester.makepyfile("def test_this(): assert 1") + result = pytester.runpytest("--color=no") assert "test session starts" in result.stdout.str() result.stdout.no_fnmatch_line("*\x1b[1m*") @pytest.mark.parametrize("verbose", [True, False]) -def test_color_yes_collection_on_non_atty(testdir, verbose): +def test_color_yes_collection_on_non_atty(pytester: Pytester, verbose) -> None: """#1397: Skip collect progress report when working on non-terminals.""" - testdir.makepyfile( + pytester.makepyfile( """ import pytest @pytest.mark.parametrize('i', range(10)) @@ -1152,7 +1173,7 @@ def test_this(i): args = ["--color=yes"] if verbose: args.append("-vv") - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) assert "test session starts" in result.stdout.str() assert "\x1b[1m" in result.stdout.str() result.stdout.no_fnmatch_line("*collecting 10 items*") @@ -1220,9 +1241,9 @@ class Option: assert getreportopt(config) == "fE" -def test_terminalreporter_reportopt_addopts(testdir): - testdir.makeini("[pytest]\naddopts=-rs") - testdir.makepyfile( +def test_terminalreporter_reportopt_addopts(pytester: Pytester) -> None: + pytester.makeini("[pytest]\naddopts=-rs") + pytester.makepyfile( """ import pytest @@ -1235,12 +1256,12 @@ def test_opt(tr): assert not tr.hasopt('qwe') """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) -def test_tbstyle_short(testdir): - p = testdir.makepyfile( +def test_tbstyle_short(pytester: Pytester) -> None: + p = pytester.makepyfile( """ import pytest @@ -1252,19 +1273,19 @@ def test_opt(arg): assert x """ ) - result = testdir.runpytest("--tb=short") + result = pytester.runpytest("--tb=short") s = result.stdout.str() assert "arg = 42" not in s assert "x = 0" not in s - result.stdout.fnmatch_lines(["*%s:8*" % p.basename, " assert x", "E assert*"]) - result = testdir.runpytest() + result.stdout.fnmatch_lines(["*%s:8*" % p.name, " assert x", "E assert*"]) + result = pytester.runpytest() s = result.stdout.str() assert "x = 0" in s assert "assert x" in s -def test_traceconfig(testdir): - result = testdir.runpytest("--traceconfig") +def test_traceconfig(pytester: Pytester) -> None: + result = pytester.runpytest("--traceconfig") result.stdout.fnmatch_lines(["*active plugins*"]) assert result.ret == ExitCode.NO_TESTS_COLLECTED @@ -1273,15 +1294,15 @@ class TestGenericReporting: """Test class which can be subclassed with a different option provider to run e.g. distributed tests.""" - def test_collect_fail(self, testdir, option): - testdir.makepyfile("import xyz\n") - result = testdir.runpytest(*option.args) + def test_collect_fail(self, pytester: Pytester, option) -> None: + pytester.makepyfile("import xyz\n") + result = pytester.runpytest(*option.args) result.stdout.fnmatch_lines( ["ImportError while importing*", "*No module named *xyz*", "*1 error*"] ) - def test_maxfailures(self, testdir, option): - testdir.makepyfile( + def test_maxfailures(self, pytester: Pytester, option) -> None: + pytester.makepyfile( """ def test_1(): assert 0 @@ -1291,7 +1312,7 @@ def test_3(): assert 0 """ ) - result = testdir.runpytest("--maxfail=2", *option.args) + result = pytester.runpytest("--maxfail=2", *option.args) result.stdout.fnmatch_lines( [ "*def test_1():*", @@ -1301,15 +1322,15 @@ def test_3(): ] ) - def test_maxfailures_with_interrupted(self, testdir): - testdir.makepyfile( + def test_maxfailures_with_interrupted(self, pytester: Pytester) -> None: + pytester.makepyfile( """ def test(request): request.session.shouldstop = "session_interrupted" assert 0 """ ) - result = testdir.runpytest("--maxfail=1", "-ra") + result = pytester.runpytest("--maxfail=1", "-ra") result.stdout.fnmatch_lines( [ "*= short test summary info =*", @@ -1320,8 +1341,8 @@ def test(request): ] ) - def test_tb_option(self, testdir, option): - testdir.makepyfile( + def test_tb_option(self, pytester: Pytester, option) -> None: + pytester.makepyfile( """ import pytest def g(): @@ -1333,7 +1354,7 @@ def test_func(): ) for tbopt in ["long", "short", "no"]: print("testing --tb=%s..." % tbopt) - result = testdir.runpytest("-rN", "--tb=%s" % tbopt) + result = pytester.runpytest("-rN", "--tb=%s" % tbopt) s = result.stdout.str() if tbopt == "long": assert "print(6*7)" in s @@ -1347,8 +1368,8 @@ def test_func(): assert "--calling--" not in s assert "IndexError" not in s - def test_tb_crashline(self, testdir, option): - p = testdir.makepyfile( + def test_tb_crashline(self, pytester: Pytester, option) -> None: + p = pytester.makepyfile( """ import pytest def g(): @@ -1360,16 +1381,16 @@ def test_func2(): assert 0, "hello" """ ) - result = testdir.runpytest("--tb=line") - bn = p.basename + result = pytester.runpytest("--tb=line") + bn = p.name result.stdout.fnmatch_lines( ["*%s:3: IndexError*" % bn, "*%s:8: AssertionError: hello*" % bn] ) s = result.stdout.str() assert "def test_func2" not in s - def test_pytest_report_header(self, testdir, option): - testdir.makeconftest( + def test_pytest_report_header(self, pytester: Pytester, option) -> None: + pytester.makeconftest( """ def pytest_sessionstart(session): session.config._somevalue = 42 @@ -1377,17 +1398,17 @@ def pytest_report_header(config): return "hello: %s" % config._somevalue """ ) - testdir.mkdir("a").join("conftest.py").write( + pytester.mkdir("a").joinpath("conftest.py").write_text( """ def pytest_report_header(config, startdir): return ["line1", str(startdir)] """ ) - result = testdir.runpytest("a") - result.stdout.fnmatch_lines(["*hello: 42*", "line1", str(testdir.tmpdir)]) + result = pytester.runpytest("a") + result.stdout.fnmatch_lines(["*hello: 42*", "line1", str(pytester.path)]) - def test_show_capture(self, testdir): - testdir.makepyfile( + def test_show_capture(self, pytester: Pytester) -> None: + pytester.makepyfile( """ import sys import logging @@ -1399,7 +1420,7 @@ def test_one(): """ ) - result = testdir.runpytest("--tb=short") + result = pytester.runpytest("--tb=short") result.stdout.fnmatch_lines( [ "!This is stdout!", @@ -1408,7 +1429,7 @@ def test_one(): ] ) - result = testdir.runpytest("--show-capture=all", "--tb=short") + result = pytester.runpytest("--show-capture=all", "--tb=short") result.stdout.fnmatch_lines( [ "!This is stdout!", @@ -1417,29 +1438,29 @@ def test_one(): ] ) - stdout = testdir.runpytest("--show-capture=stdout", "--tb=short").stdout.str() + stdout = pytester.runpytest("--show-capture=stdout", "--tb=short").stdout.str() assert "!This is stderr!" not in stdout assert "!This is stdout!" in stdout assert "!This is a warning log msg!" not in stdout - stdout = testdir.runpytest("--show-capture=stderr", "--tb=short").stdout.str() + stdout = pytester.runpytest("--show-capture=stderr", "--tb=short").stdout.str() assert "!This is stdout!" not in stdout assert "!This is stderr!" in stdout assert "!This is a warning log msg!" not in stdout - stdout = testdir.runpytest("--show-capture=log", "--tb=short").stdout.str() + stdout = pytester.runpytest("--show-capture=log", "--tb=short").stdout.str() assert "!This is stdout!" not in stdout assert "!This is stderr!" not in stdout assert "!This is a warning log msg!" in stdout - stdout = testdir.runpytest("--show-capture=no", "--tb=short").stdout.str() + stdout = pytester.runpytest("--show-capture=no", "--tb=short").stdout.str() assert "!This is stdout!" not in stdout assert "!This is stderr!" not in stdout assert "!This is a warning log msg!" not in stdout - def test_show_capture_with_teardown_logs(self, testdir): + def test_show_capture_with_teardown_logs(self, pytester: Pytester) -> None: """Ensure that the capturing of teardown logs honor --show-capture setting""" - testdir.makepyfile( + pytester.makepyfile( """ import logging import sys @@ -1457,30 +1478,30 @@ def test_func(): """ ) - result = testdir.runpytest("--show-capture=stdout", "--tb=short").stdout.str() + result = pytester.runpytest("--show-capture=stdout", "--tb=short").stdout.str() assert "!stdout!" in result assert "!stderr!" not in result assert "!log!" not in result - result = testdir.runpytest("--show-capture=stderr", "--tb=short").stdout.str() + result = pytester.runpytest("--show-capture=stderr", "--tb=short").stdout.str() assert "!stdout!" not in result assert "!stderr!" in result assert "!log!" not in result - result = testdir.runpytest("--show-capture=log", "--tb=short").stdout.str() + result = pytester.runpytest("--show-capture=log", "--tb=short").stdout.str() assert "!stdout!" not in result assert "!stderr!" not in result assert "!log!" in result - result = testdir.runpytest("--show-capture=no", "--tb=short").stdout.str() + result = pytester.runpytest("--show-capture=no", "--tb=short").stdout.str() assert "!stdout!" not in result assert "!stderr!" not in result assert "!log!" not in result @pytest.mark.xfail("not hasattr(os, 'dup')") -def test_fdopen_kept_alive_issue124(testdir): - testdir.makepyfile( +def test_fdopen_kept_alive_issue124(pytester: Pytester) -> None: + pytester.makepyfile( """ import os, sys k = [] @@ -1493,12 +1514,12 @@ def test_close_kept_alive_file(): stdout.close() """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 passed*"]) -def test_tbstyle_native_setup_error(testdir): - testdir.makepyfile( +def test_tbstyle_native_setup_error(pytester: Pytester) -> None: + pytester.makepyfile( """ import pytest @pytest.fixture @@ -1509,14 +1530,14 @@ def test_error_fixture(setup_error_fixture): pass """ ) - result = testdir.runpytest("--tb=native") + result = pytester.runpytest("--tb=native") result.stdout.fnmatch_lines( ['*File *test_tbstyle_native_setup_error.py", line *, in setup_error_fixture*'] ) -def test_terminal_summary(testdir): - testdir.makeconftest( +def test_terminal_summary(pytester: Pytester) -> None: + pytester.makeconftest( """ def pytest_terminal_summary(terminalreporter, exitstatus): w = terminalreporter @@ -1525,7 +1546,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus): w.line("exitstatus: {0}".format(exitstatus)) """ ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines( """ *==== hello ====* @@ -1536,18 +1557,18 @@ def pytest_terminal_summary(terminalreporter, exitstatus): @pytest.mark.filterwarnings("default") -def test_terminal_summary_warnings_are_displayed(testdir): +def test_terminal_summary_warnings_are_displayed(pytester: Pytester) -> None: """Test that warnings emitted during pytest_terminal_summary are displayed. (#1305). """ - testdir.makeconftest( + pytester.makeconftest( """ import warnings def pytest_terminal_summary(terminalreporter): warnings.warn(UserWarning('internal warning')) """ ) - testdir.makepyfile( + pytester.makepyfile( """ def test_failure(): import warnings @@ -1555,7 +1576,7 @@ def test_failure(): assert 0 """ ) - result = testdir.runpytest("-ra") + result = pytester.runpytest("-ra") result.stdout.fnmatch_lines( [ "*= warnings summary =*", @@ -1573,8 +1594,8 @@ def test_failure(): @pytest.mark.filterwarnings("default") -def test_terminal_summary_warnings_header_once(testdir): - testdir.makepyfile( +def test_terminal_summary_warnings_header_once(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_failure(): import warnings @@ -1582,7 +1603,7 @@ def test_failure(): assert 0 """ ) - result = testdir.runpytest("-ra") + result = pytester.runpytest("-ra") result.stdout.fnmatch_lines( [ "*= warnings summary =*", @@ -1598,8 +1619,8 @@ def test_failure(): @pytest.mark.filterwarnings("default") -def test_terminal_no_summary_warnings_header_once(testdir): - testdir.makepyfile( +def test_terminal_no_summary_warnings_header_once(pytester: Pytester) -> None: + pytester.makepyfile( """ def test_failure(): import warnings @@ -1607,7 +1628,7 @@ def test_failure(): assert 0 """ ) - result = testdir.runpytest("--no-summary") + result = pytester.runpytest("--no-summary") result.stdout.no_fnmatch_line("*= warnings summary =*") result.stdout.no_fnmatch_line("*= short test summary info =*") @@ -1796,8 +1817,8 @@ class TestClassicOutputStyle: """Ensure classic output style works as expected (#3883)""" @pytest.fixture - def test_files(self, testdir): - testdir.makepyfile( + def test_files(self, pytester: Pytester) -> None: + pytester.makepyfile( **{ "test_one.py": "def test_one(): pass", "test_two.py": "def test_two(): assert 0", @@ -1809,8 +1830,8 @@ def test_three_3(): pass } ) - def test_normal_verbosity(self, testdir, test_files): - result = testdir.runpytest("-o", "console_output_style=classic") + def test_normal_verbosity(self, pytester: Pytester, test_files) -> None: + result = pytester.runpytest("-o", "console_output_style=classic") result.stdout.fnmatch_lines( [ "test_one.py .", @@ -1820,8 +1841,8 @@ def test_normal_verbosity(self, testdir, test_files): ] ) - def test_verbose(self, testdir, test_files): - result = testdir.runpytest("-o", "console_output_style=classic", "-v") + def test_verbose(self, pytester: Pytester, test_files) -> None: + result = pytester.runpytest("-o", "console_output_style=classic", "-v") result.stdout.fnmatch_lines( [ "test_one.py::test_one PASSED", @@ -1833,15 +1854,15 @@ def test_verbose(self, testdir, test_files): ] ) - def test_quiet(self, testdir, test_files): - result = testdir.runpytest("-o", "console_output_style=classic", "-q") + def test_quiet(self, pytester: Pytester, test_files) -> None: + result = pytester.runpytest("-o", "console_output_style=classic", "-q") result.stdout.fnmatch_lines([".F.F.", "*2 failed, 3 passed in*"]) class TestProgressOutputStyle: @pytest.fixture - def many_tests_files(self, testdir): - testdir.makepyfile( + def many_tests_files(self, pytester: Pytester) -> None: + pytester.makepyfile( test_bar=""" import pytest @pytest.mark.parametrize('i', range(10)) @@ -1859,10 +1880,10 @@ def test_foobar(i): pass """, ) - def test_zero_tests_collected(self, testdir): + def test_zero_tests_collected(self, pytester: Pytester) -> None: """Some plugins (testmon for example) might issue pytest_runtest_logreport without any tests being actually collected (#2971).""" - testdir.makeconftest( + pytester.makeconftest( """ def pytest_collection_modifyitems(items, config): from _pytest.runner import CollectReport @@ -1873,12 +1894,12 @@ def pytest_collection_modifyitems(items, config): config.hook.pytest_runtest_logreport(report=rep) """ ) - output = testdir.runpytest() + output = pytester.runpytest() output.stdout.no_fnmatch_line("*ZeroDivisionError*") output.stdout.fnmatch_lines(["=* 2 passed in *="]) - def test_normal(self, many_tests_files, testdir): - output = testdir.runpytest() + def test_normal(self, many_tests_files, pytester: Pytester) -> None: + output = pytester.runpytest() output.stdout.re_match_lines( [ r"test_bar.py \.{10} \s+ \[ 50%\]", @@ -1887,9 +1908,11 @@ def test_normal(self, many_tests_files, testdir): ] ) - def test_colored_progress(self, testdir, monkeypatch, color_mapping): + def test_colored_progress( + self, pytester: Pytester, monkeypatch, color_mapping + ) -> None: monkeypatch.setenv("PY_COLORS", "1") - testdir.makepyfile( + pytester.makepyfile( test_axfail=""" import pytest @pytest.mark.xfail @@ -1914,7 +1937,7 @@ def test_foo(i): def test_foobar(i): raise ValueError() """, ) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.re_match_lines( color_mapping.format_for_rematch( [ @@ -1927,7 +1950,7 @@ def test_foobar(i): raise ValueError() ) # Only xfail should have yellow progress indicator. - result = testdir.runpytest("test_axfail.py") + result = pytester.runpytest("test_axfail.py") result.stdout.re_match_lines( color_mapping.format_for_rematch( [ @@ -1937,14 +1960,14 @@ def test_foobar(i): raise ValueError() ) ) - def test_count(self, many_tests_files, testdir): - testdir.makeini( + def test_count(self, many_tests_files, pytester: Pytester) -> None: + pytester.makeini( """ [pytest] console_output_style = count """ ) - output = testdir.runpytest() + output = pytester.runpytest() output.stdout.re_match_lines( [ r"test_bar.py \.{10} \s+ \[10/20\]", @@ -1953,8 +1976,8 @@ def test_count(self, many_tests_files, testdir): ] ) - def test_verbose(self, many_tests_files, testdir): - output = testdir.runpytest("-v") + def test_verbose(self, many_tests_files, pytester: Pytester) -> None: + output = pytester.runpytest("-v") output.stdout.re_match_lines( [ r"test_bar.py::test_bar\[0\] PASSED \s+ \[ 5%\]", @@ -1963,14 +1986,14 @@ def test_verbose(self, many_tests_files, testdir): ] ) - def test_verbose_count(self, many_tests_files, testdir): - testdir.makeini( + def test_verbose_count(self, many_tests_files, pytester: Pytester) -> None: + pytester.makeini( """ [pytest] console_output_style = count """ ) - output = testdir.runpytest("-v") + output = pytester.runpytest("-v") output.stdout.re_match_lines( [ r"test_bar.py::test_bar\[0\] PASSED \s+ \[ 1/20\]", @@ -1979,28 +2002,34 @@ def test_verbose_count(self, many_tests_files, testdir): ] ) - def test_xdist_normal(self, many_tests_files, testdir, monkeypatch): + def test_xdist_normal( + self, many_tests_files, pytester: Pytester, monkeypatch + ) -> None: pytest.importorskip("xdist") monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) - output = testdir.runpytest("-n2") + output = pytester.runpytest("-n2") output.stdout.re_match_lines([r"\.{20} \s+ \[100%\]"]) - def test_xdist_normal_count(self, many_tests_files, testdir, monkeypatch): + def test_xdist_normal_count( + self, many_tests_files, pytester: Pytester, monkeypatch + ) -> None: pytest.importorskip("xdist") monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) - testdir.makeini( + pytester.makeini( """ [pytest] console_output_style = count """ ) - output = testdir.runpytest("-n2") + output = pytester.runpytest("-n2") output.stdout.re_match_lines([r"\.{20} \s+ \[20/20\]"]) - def test_xdist_verbose(self, many_tests_files, testdir, monkeypatch): + def test_xdist_verbose( + self, many_tests_files, pytester: Pytester, monkeypatch + ) -> None: pytest.importorskip("xdist") monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) - output = testdir.runpytest("-n2", "-v") + output = pytester.runpytest("-n2", "-v") output.stdout.re_match_lines_random( [ r"\[gw\d\] \[\s*\d+%\] PASSED test_bar.py::test_bar\[1\]", @@ -2025,13 +2054,13 @@ def test_xdist_verbose(self, many_tests_files, testdir, monkeypatch): ] ) - def test_capture_no(self, many_tests_files, testdir): - output = testdir.runpytest("-s") + def test_capture_no(self, many_tests_files, pytester: Pytester) -> None: + output = pytester.runpytest("-s") output.stdout.re_match_lines( [r"test_bar.py \.{10}", r"test_foo.py \.{5}", r"test_foobar.py \.{5}"] ) - output = testdir.runpytest("--capture=no") + output = pytester.runpytest("--capture=no") output.stdout.no_fnmatch_line("*%]*") @@ -2039,8 +2068,8 @@ class TestProgressWithTeardown: """Ensure we show the correct percentages for tests that fail during teardown (#3088)""" @pytest.fixture - def contest_with_teardown_fixture(self, testdir): - testdir.makeconftest( + def contest_with_teardown_fixture(self, pytester: Pytester) -> None: + pytester.makeconftest( """ import pytest @@ -2052,8 +2081,8 @@ def fail_teardown(): ) @pytest.fixture - def many_files(self, testdir, contest_with_teardown_fixture): - testdir.makepyfile( + def many_files(self, pytester: Pytester, contest_with_teardown_fixture) -> None: + pytester.makepyfile( test_bar=""" import pytest @pytest.mark.parametrize('i', range(5)) @@ -2068,26 +2097,28 @@ def test_foo(fail_teardown, i): """, ) - def test_teardown_simple(self, testdir, contest_with_teardown_fixture): - testdir.makepyfile( + def test_teardown_simple( + self, pytester: Pytester, contest_with_teardown_fixture + ) -> None: + pytester.makepyfile( """ def test_foo(fail_teardown): pass """ ) - output = testdir.runpytest() + output = pytester.runpytest() output.stdout.re_match_lines([r"test_teardown_simple.py \.E\s+\[100%\]"]) def test_teardown_with_test_also_failing( - self, testdir, contest_with_teardown_fixture - ): - testdir.makepyfile( + self, pytester: Pytester, contest_with_teardown_fixture + ) -> None: + pytester.makepyfile( """ def test_foo(fail_teardown): assert 0 """ ) - output = testdir.runpytest("-rfE") + output = pytester.runpytest("-rfE") output.stdout.re_match_lines( [ r"test_teardown_with_test_also_failing.py FE\s+\[100%\]", @@ -2096,16 +2127,16 @@ def test_foo(fail_teardown): ] ) - def test_teardown_many(self, testdir, many_files): - output = testdir.runpytest() + def test_teardown_many(self, pytester: Pytester, many_files) -> None: + output = pytester.runpytest() output.stdout.re_match_lines( [r"test_bar.py (\.E){5}\s+\[ 25%\]", r"test_foo.py (\.E){15}\s+\[100%\]"] ) def test_teardown_many_verbose( - self, testdir: Testdir, many_files, color_mapping + self, pytester: Pytester, many_files, color_mapping ) -> None: - result = testdir.runpytest("-v") + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ @@ -2119,10 +2150,10 @@ def test_teardown_many_verbose( ) ) - def test_xdist_normal(self, many_files, testdir, monkeypatch): + def test_xdist_normal(self, many_files, pytester: Pytester, monkeypatch) -> None: pytest.importorskip("xdist") monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) - output = testdir.runpytest("-n2") + output = pytester.runpytest("-n2") output.stdout.re_match_lines([r"[\.E]{40} \s+ \[100%\]"]) @@ -2160,7 +2191,7 @@ class X: assert reason == message -def test_line_with_reprcrash(monkeypatch): +def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None: mocked_verbose_word = "FAILED" mocked_pos = "some::nodeid" @@ -2242,9 +2273,9 @@ def test_format_session_duration(seconds, expected): assert format_session_duration(seconds) == expected -def test_collecterror(testdir): - p1 = testdir.makepyfile("raise SyntaxError()") - result = testdir.runpytest("-ra", str(p1)) +def test_collecterror(pytester: Pytester) -> None: + p1 = pytester.makepyfile("raise SyntaxError()") + result = pytester.runpytest("-ra", str(p1)) result.stdout.fnmatch_lines( [ "collected 0 items / 1 error", @@ -2259,29 +2290,29 @@ def test_collecterror(testdir): ) -def test_no_summary_collecterror(testdir): - p1 = testdir.makepyfile("raise SyntaxError()") - result = testdir.runpytest("-ra", "--no-summary", str(p1)) +def test_no_summary_collecterror(pytester: Pytester) -> None: + p1 = pytester.makepyfile("raise SyntaxError()") + result = pytester.runpytest("-ra", "--no-summary", str(p1)) result.stdout.no_fnmatch_line("*= ERRORS =*") -def test_via_exec(testdir: Testdir) -> None: - p1 = testdir.makepyfile("exec('def test_via_exec(): pass')") - result = testdir.runpytest(str(p1), "-vv") +def test_via_exec(pytester: Pytester) -> None: + p1 = pytester.makepyfile("exec('def test_via_exec(): pass')") + result = pytester.runpytest(str(p1), "-vv") result.stdout.fnmatch_lines( ["test_via_exec.py::test_via_exec <- PASSED*", "*= 1 passed in *"] ) class TestCodeHighlight: - def test_code_highlight_simple(self, testdir: Testdir, color_mapping) -> None: - testdir.makepyfile( + def test_code_highlight_simple(self, pytester: Pytester, color_mapping) -> None: + pytester.makepyfile( """ def test_foo(): assert 1 == 10 """ ) - result = testdir.runpytest("--color=yes") + result = pytester.runpytest("--color=yes") result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ @@ -2292,15 +2323,17 @@ def test_foo(): ) ) - def test_code_highlight_continuation(self, testdir: Testdir, color_mapping) -> None: - testdir.makepyfile( + def test_code_highlight_continuation( + self, pytester: Pytester, color_mapping + ) -> None: + pytester.makepyfile( """ def test_foo(): print(''' '''); assert 0 """ ) - result = testdir.runpytest("--color=yes") + result = pytester.runpytest("--color=yes") result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( From 612f157dbd021c231cca24726e53ed5c50debe48 Mon Sep 17 00:00:00 2001 From: Katarzyna Date: Sun, 10 May 2020 03:09:09 +0200 Subject: [PATCH 813/823] Show reason for skipped test in verbose mode --- changelog/2044.improvement.rst | 1 + src/_pytest/terminal.py | 82 +++++++++++++++++++++++++--------- testing/test_terminal.py | 55 +++++++++++++++++++++++ 3 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 changelog/2044.improvement.rst diff --git a/changelog/2044.improvement.rst b/changelog/2044.improvement.rst new file mode 100644 index 00000000000..c9e47c3f604 --- /dev/null +++ b/changelog/2044.improvement.rst @@ -0,0 +1 @@ +Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS". diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 7d2943dd01e..2e68e257548 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -13,6 +13,7 @@ from pathlib import Path from typing import Any from typing import Callable +from typing import cast from typing import Dict from typing import Generator from typing import List @@ -545,6 +546,16 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: line = self._locationline(rep.nodeid, *rep.location) if not running_xdist: self.write_ensure_prefix(line, word, **markup) + if rep.skipped or hasattr(report, "wasxfail"): + available_width = ( + (self._tw.fullwidth - self._tw.width_of_current_line) + - len(" [100%]") + - 1 + ) + reason = _get_raw_skip_reason(rep) + reason_ = _format_trimmed(" ({})", reason, available_width) + if reason_ is not None: + self._tw.write(reason_) if self._show_progress_info: self._write_progress_information_filling_space() else: @@ -1249,6 +1260,31 @@ def _get_pos(config: Config, rep: BaseReport): return nodeid +def _format_trimmed(format: str, msg: str, available_width: int) -> Optional[str]: + """Format msg into format, ellipsizing it if doesn't fit in available_width. + + Returns None if even the ellipsis can't fit. + """ + # Only use the first line. + i = msg.find("\n") + if i != -1: + msg = msg[:i] + + ellipsis = "..." + format_width = wcswidth(format.format("")) + if format_width + len(ellipsis) > available_width: + return None + + if format_width + wcswidth(msg) > available_width: + available_width -= len(ellipsis) + msg = msg[:available_width] + while format_width + wcswidth(msg) > available_width: + msg = msg[:-1] + msg += ellipsis + + return format.format(msg) + + def _get_line_with_reprcrash_message( config: Config, rep: BaseReport, termwidth: int ) -> str: @@ -1257,11 +1293,7 @@ def _get_line_with_reprcrash_message( pos = _get_pos(config, rep) line = f"{verbose_word} {pos}" - len_line = wcswidth(line) - ellipsis, len_ellipsis = "...", 3 - if len_line > termwidth - len_ellipsis: - # No space for an additional message. - return line + line_width = wcswidth(line) try: # Type ignored intentionally -- possible AttributeError expected. @@ -1269,22 +1301,11 @@ def _get_line_with_reprcrash_message( except AttributeError: pass else: - # Only use the first line. - i = msg.find("\n") - if i != -1: - msg = msg[:i] - len_msg = wcswidth(msg) - - sep, len_sep = " - ", 3 - max_len_msg = termwidth - len_line - len_sep - if max_len_msg >= len_ellipsis: - if len_msg > max_len_msg: - max_len_msg -= len_ellipsis - msg = msg[:max_len_msg] - while wcswidth(msg) > max_len_msg: - msg = msg[:-1] - msg += ellipsis - line += sep + msg + available_width = termwidth - line_width + msg = _format_trimmed(" - {}", msg, available_width) + if msg is not None: + line += msg + return line @@ -1361,3 +1382,22 @@ def format_session_duration(seconds: float) -> str: else: dt = datetime.timedelta(seconds=int(seconds)) return f"{seconds:.2f}s ({dt})" + + +def _get_raw_skip_reason(report: TestReport) -> str: + """Get the reason string of a skip/xfail/xpass test report. + + The string is just the part given by the user. + """ + if hasattr(report, "wasxfail"): + reason = cast(str, report.wasxfail) + if reason.startswith("reason: "): + reason = reason[len("reason: ") :] + return reason + else: + assert report.skipped + assert isinstance(report.longrepr, tuple) + _, _, reason = report.longrepr + if reason.startswith("Skipped: "): + reason = reason[len("Skipped: ") :] + return reason diff --git a/testing/test_terminal.py b/testing/test_terminal.py index a4d22d2aa3b..fdd4301f94f 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -5,6 +5,7 @@ import textwrap from io import StringIO from pathlib import Path +from types import SimpleNamespace from typing import cast from typing import Dict from typing import List @@ -23,8 +24,11 @@ from _pytest.pytester import Pytester from _pytest.reports import BaseReport from _pytest.reports import CollectReport +from _pytest.reports import TestReport from _pytest.terminal import _folded_skips +from _pytest.terminal import _format_trimmed from _pytest.terminal import _get_line_with_reprcrash_message +from _pytest.terminal import _get_raw_skip_reason from _pytest.terminal import _plugin_nameversions from _pytest.terminal import getreportopt from _pytest.terminal import TerminalReporter @@ -342,6 +346,33 @@ def test_foobar(): color_mapping.format_for_fnmatch(["*{red}FOO{reset}*"]) ) + def test_verbose_skip_reason(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.skip(reason="123") + def test_1(): + pass + + @pytest.mark.xfail(reason="456") + def test_2(): + pass + + @pytest.mark.xfail(reason="789") + def test_3(): + assert False + """ + ) + result = pytester.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "test_verbose_skip_reason.py::test_1 SKIPPED (123) *", + "test_verbose_skip_reason.py::test_2 XPASS (456) *", + "test_verbose_skip_reason.py::test_3 XFAIL (789) *", + ] + ) + class TestCollectonly: def test_collectonly_basic(self, pytester: Pytester) -> None: @@ -2345,3 +2376,27 @@ def test_foo(): ] ) ) + + +def test_raw_skip_reason_skipped() -> None: + report = SimpleNamespace() + report.skipped = True + report.longrepr = ("xyz", 3, "Skipped: Just so") + + reason = _get_raw_skip_reason(cast(TestReport, report)) + assert reason == "Just so" + + +def test_raw_skip_reason_xfail() -> None: + report = SimpleNamespace() + report.wasxfail = "reason: To everything there is a season" + + reason = _get_raw_skip_reason(cast(TestReport, report)) + assert reason == "To everything there is a season" + + +def test_format_trimmed() -> None: + msg = "unconditional skip" + + assert _format_trimmed(" ({}) ", msg, len(msg) + 4) == " (unconditional skip) " + assert _format_trimmed(" ({}) ", msg, len(msg) + 3) == " (unconditional ...) " From b16c0912537bee06c83e202112f4b036e4fd66dc Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 27 Aug 2020 17:52:16 +0100 Subject: [PATCH 814/823] Add `pytest_markeval_namespace` hook. Add a new hook , `pytest_markeval_namespace` which should return a dictionary. This dictionary will be used to augment the "global" variables available to evaluate skipif/xfail/xpass markers. Pseudo example ``conftest.py``: .. code-block:: python def pytest_markeval_namespace(): return {"color": "red"} ``test_func.py``: .. code-block:: python @pytest.mark.skipif("color == 'blue'", reason="Color is not red") def test_func(): assert False --- changelog/7695.feature.rst | 19 +++++ src/_pytest/hookspec.py | 21 +++++ src/_pytest/skipping.py | 11 +++ testing/test_skipping.py | 158 +++++++++++++++++++++++++++++++++++++ 4 files changed, 209 insertions(+) create mode 100644 changelog/7695.feature.rst diff --git a/changelog/7695.feature.rst b/changelog/7695.feature.rst new file mode 100644 index 00000000000..ec8632fc82a --- /dev/null +++ b/changelog/7695.feature.rst @@ -0,0 +1,19 @@ +A new hook was added, `pytest_markeval_namespace` which should return a dictionary. +This dictionary will be used to augment the "global" variables available to evaluate skipif/xfail/xpass markers. + +Pseudo example + +``conftest.py``: + +.. code-block:: python + + def pytest_markeval_namespace(): + return {"color": "red"} + +``test_func.py``: + +.. code-block:: python + + @pytest.mark.skipif("color == 'blue'", reason="Color is not red") + def test_func(): + assert False diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 33ca782cf49..e499b742c7e 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -808,6 +808,27 @@ def pytest_warning_recorded( """ +# ------------------------------------------------------------------------- +# Hooks for influencing skipping +# ------------------------------------------------------------------------- + + +def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]: + """Called when constructing the globals dictionary used for + evaluating string conditions in xfail/skipif markers. + + This is useful when the condition for a marker requires + objects that are expensive or impossible to obtain during + collection time, which is required by normal boolean + conditions. + + .. versionadded:: 6.2 + + :param _pytest.config.Config config: The pytest config object. + :returns: A dictionary of additional globals to add. + """ + + # ------------------------------------------------------------------------- # error handling and internal debugging hooks # ------------------------------------------------------------------------- diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index afc3610eb4c..9aacfecee7a 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -3,6 +3,7 @@ import platform import sys import traceback +from collections.abc import Mapping from typing import Generator from typing import Optional from typing import Tuple @@ -98,6 +99,16 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, "platform": platform, "config": item.config, } + for dictionary in reversed( + item.ihook.pytest_markeval_namespace(config=item.config) + ): + if not isinstance(dictionary, Mapping): + raise ValueError( + "pytest_markeval_namespace() needs to return a dict, got {!r}".format( + dictionary + ) + ) + globals_.update(dictionary) if hasattr(item, "obj"): globals_.update(item.obj.__globals__) # type: ignore[attr-defined] try: diff --git a/testing/test_skipping.py b/testing/test_skipping.py index cfc0cdbca5e..fc66eb18e64 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1,4 +1,5 @@ import sys +import textwrap import pytest from _pytest.pytester import Pytester @@ -155,6 +156,136 @@ def test_func(self): assert skipped assert skipped.reason == "condition: config._hackxyz" + def test_skipif_markeval_namespace(self, pytester: Pytester) -> None: + pytester.makeconftest( + """ + import pytest + + def pytest_markeval_namespace(): + return {"color": "green"} + """ + ) + p = pytester.makepyfile( + """ + import pytest + + @pytest.mark.skipif("color == 'green'") + def test_1(): + assert True + + @pytest.mark.skipif("color == 'red'") + def test_2(): + assert True + """ + ) + res = pytester.runpytest(p) + assert res.ret == 0 + res.stdout.fnmatch_lines(["*1 skipped*"]) + res.stdout.fnmatch_lines(["*1 passed*"]) + + def test_skipif_markeval_namespace_multiple(self, pytester: Pytester) -> None: + """Keys defined by ``pytest_markeval_namespace()`` in nested plugins override top-level ones.""" + root = pytester.mkdir("root") + root.joinpath("__init__.py").touch() + root.joinpath("conftest.py").write_text( + textwrap.dedent( + """\ + import pytest + + def pytest_markeval_namespace(): + return {"arg": "root"} + """ + ) + ) + root.joinpath("test_root.py").write_text( + textwrap.dedent( + """\ + import pytest + + @pytest.mark.skipif("arg == 'root'") + def test_root(): + assert False + """ + ) + ) + foo = root.joinpath("foo") + foo.mkdir() + foo.joinpath("__init__.py").touch() + foo.joinpath("conftest.py").write_text( + textwrap.dedent( + """\ + import pytest + + def pytest_markeval_namespace(): + return {"arg": "foo"} + """ + ) + ) + foo.joinpath("test_foo.py").write_text( + textwrap.dedent( + """\ + import pytest + + @pytest.mark.skipif("arg == 'foo'") + def test_foo(): + assert False + """ + ) + ) + bar = root.joinpath("bar") + bar.mkdir() + bar.joinpath("__init__.py").touch() + bar.joinpath("conftest.py").write_text( + textwrap.dedent( + """\ + import pytest + + def pytest_markeval_namespace(): + return {"arg": "bar"} + """ + ) + ) + bar.joinpath("test_bar.py").write_text( + textwrap.dedent( + """\ + import pytest + + @pytest.mark.skipif("arg == 'bar'") + def test_bar(): + assert False + """ + ) + ) + + reprec = pytester.inline_run("-vs", "--capture=no") + reprec.assertoutcome(skipped=3) + + def test_skipif_markeval_namespace_ValueError(self, pytester: Pytester) -> None: + pytester.makeconftest( + """ + import pytest + + def pytest_markeval_namespace(): + return True + """ + ) + p = pytester.makepyfile( + """ + import pytest + + @pytest.mark.skipif("color == 'green'") + def test_1(): + assert True + """ + ) + res = pytester.runpytest(p) + assert res.ret == 1 + res.stdout.fnmatch_lines( + [ + "*ValueError: pytest_markeval_namespace() needs to return a dict, got True*" + ] + ) + class TestXFail: @pytest.mark.parametrize("strict", [True, False]) @@ -577,6 +708,33 @@ def test_foo(): result.stdout.fnmatch_lines(["*1 failed*" if strict else "*1 xpassed*"]) assert result.ret == (1 if strict else 0) + def test_xfail_markeval_namespace(self, pytester: Pytester) -> None: + pytester.makeconftest( + """ + import pytest + + def pytest_markeval_namespace(): + return {"color": "green"} + """ + ) + p = pytester.makepyfile( + """ + import pytest + + @pytest.mark.xfail("color == 'green'") + def test_1(): + assert False + + @pytest.mark.xfail("color == 'red'") + def test_2(): + assert False + """ + ) + res = pytester.runpytest(p) + assert res.ret == 1 + res.stdout.fnmatch_lines(["*1 failed*"]) + res.stdout.fnmatch_lines(["*1 xfailed*"]) + class TestXFailwithSetupTeardown: def test_failing_setup_issue9(self, pytester: Pytester) -> None: From cf1051cfbae401a020ccdbda59c500c1d0e95d67 Mon Sep 17 00:00:00 2001 From: Anton <44246099+antonblr@users.noreply.github.com> Date: Sat, 12 Dec 2020 08:08:15 -0800 Subject: [PATCH 815/823] infrastructure: Stricter tox dependensies (#8119) --- .github/workflows/main.yml | 5 ++++- setup.cfg | 6 +++--- tox.ini | 16 +++++++++------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a346d653367..2b779279fdc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -165,7 +165,10 @@ jobs: with: path: ~/.cache/pre-commit key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - - run: pip install tox + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox - run: tox -e linting deploy: diff --git a/setup.cfg b/setup.cfg index 08d5853f2f5..09c07d5bb6c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ install_requires = attrs>=19.2.0 iniconfig packaging - pluggy>=0.12,<1.0 + pluggy>=0.12,<1.0.0a1 py>=1.8.2 toml atomicwrites>=1.0;sys_platform=="win32" @@ -52,8 +52,8 @@ python_requires = >=3.6 package_dir = =src setup_requires = - setuptools>=40.0 - setuptools-scm + setuptools>=>=42.0 + setuptools-scm>=3.4 zip_safe = no [options.entry_points] diff --git a/tox.ini b/tox.ini index 6c8b8b0ad95..53d4fe1b23f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] isolated_build = True -minversion = 3.5.3 +minversion = 3.20.0 distshare = {homedir}/.tox/distshare # make sure to update environment list in travis.yml and appveyor.yml envlist = @@ -16,6 +16,7 @@ envlist = py37-freeze docs docs-checklinks +requires = pip >= 20.3.1 [testenv] commands = @@ -44,19 +45,20 @@ setenv = extras = testing deps = doctesting: PyYAML - numpy: numpy - pexpect: pexpect + numpy: numpy>=1.19.4 + pexpect: pexpect>=4.8.0 pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master - pygments + pygments>=2.7.2 unittestextras: twisted unittestextras: asynctest - xdist: pytest-xdist>=1.13 + xdist: pytest-xdist>=2.1.0 + xdist: -e . {env:_PYTEST_TOX_EXTRA_DEP:} [testenv:linting] skip_install = True basepython = python3 -deps = pre-commit>=1.11.0 +deps = pre-commit>=2.9.3 commands = pre-commit run --all-files --show-diff-on-failure {posargs:} [testenv:docs] @@ -141,7 +143,7 @@ passenv = * deps = colorama github3.py - pre-commit>=1.11.0 + pre-commit>=2.9.3 wheel towncrier commands = python scripts/release.py {posargs} From f237b077fc83fe834589e619be1e898f28026eb2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 12 Dec 2020 18:31:52 +0200 Subject: [PATCH 816/823] tox: remove requires: pip>=20.3.1 Causes some trouble in CI and not really needed as old pip should still work. --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 53d4fe1b23f..be0d7ee301c 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,6 @@ envlist = py37-freeze docs docs-checklinks -requires = pip >= 20.3.1 [testenv] commands = From 6298ff1f4e21307627b59aa86437145ddcf2bafe Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 12 Dec 2020 20:26:10 +0200 Subject: [PATCH 817/823] tox: use pip legacy resolver for regen job The env var effects all of the pip installs, including regendoc which also uses setuptools-scm, so it gets the wrong version, and fails to install with the new pip resolver: ERROR: Requested regendoc from https://files.pythonhosted.org/packages/a8/5d/206e4951420bf5bbe1475c66eb06ec40d9177035e223858fee890eed0188/regendoc-0.6.1.tar.gz#sha256=db1e8c9ae02c1af559eae105bfd77ba41ed07fc8ca7030ea59db5f3f161236a4 has different version in metadata: '6.2.0' --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index be0d7ee301c..f0cfaa460fb 100644 --- a/tox.ini +++ b/tox.ini @@ -85,6 +85,9 @@ commands = changedir = doc/en basepython = python3 passenv = SETUPTOOLS_SCM_PRETEND_VERSION +# TODO: When setuptools-scm 5.0.0 is released, use SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST +# and remove the next line. +install_command=python -m pip --use-deprecated=legacy-resolver install {opts} {packages} deps = dataclasses PyYAML From 3302ff994959964c3ec2ab35ec1fe98c038684c0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 12 Dec 2020 22:09:00 +0200 Subject: [PATCH 818/823] terminal: when the skip/xfail is empty, don't show it as "()" Avoid showing a line like x.py::test_4 XPASS () [100%] which looks funny. --- src/_pytest/terminal.py | 2 +- testing/test_terminal.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2e68e257548..0e0ed70e5be 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -554,7 +554,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: ) reason = _get_raw_skip_reason(rep) reason_ = _format_trimmed(" ({})", reason, available_width) - if reason_ is not None: + if reason and reason_ is not None: self._tw.write(reason_) if self._show_progress_info: self._write_progress_information_filling_space() diff --git a/testing/test_terminal.py b/testing/test_terminal.py index fdd4301f94f..7ad5849d4b9 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -362,6 +362,10 @@ def test_2(): @pytest.mark.xfail(reason="789") def test_3(): assert False + + @pytest.mark.xfail(reason="") + def test_4(): + assert False """ ) result = pytester.runpytest("-v") @@ -370,6 +374,7 @@ def test_3(): "test_verbose_skip_reason.py::test_1 SKIPPED (123) *", "test_verbose_skip_reason.py::test_2 XPASS (456) *", "test_verbose_skip_reason.py::test_3 XFAIL (789) *", + "test_verbose_skip_reason.py::test_4 XFAIL *", ] ) From 0feeddf8edb87052402fafe690d019e3eb75dfa4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 12 Dec 2020 22:15:41 +0200 Subject: [PATCH 819/823] doc: temporary workaround for pytest-pygments lexing error pytest-pygments doesn't yet recognize the skip reason in summary line added recently. Workaround it until we get to updating it. --- doc/en/fixture.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index a0411902c0b..22b86ffcafc 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -947,7 +947,7 @@ Example: Running this test will *skip* the invocation of ``data_set`` with value ``2``: -.. code-block:: pytest +.. code-block:: $ pytest test_fixture_marks.py -v =========================== test session starts ============================ From e7073afe6e2373175130511135020af8b4e3a670 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sat, 12 Dec 2020 20:35:28 +0000 Subject: [PATCH 820/823] Prepare release version 6.2.0 --- changelog/1265.improvement.rst | 1 - changelog/2044.improvement.rst | 1 - changelog/4824.bugfix.rst | 1 - changelog/5299.feature.rst | 2 - changelog/7425.feature.rst | 5 - changelog/7429.doc.rst | 1 - changelog/7469.deprecation.rst | 18 --- changelog/7469.improvement.rst | 23 ---- changelog/7527.improvement.rst | 1 - changelog/7530.deprecation.rst | 4 - changelog/7615.improvement.rst | 1 - changelog/7695.feature.rst | 19 --- changelog/7701.improvement.rst | 1 - changelog/7710.improvement.rst | 4 - changelog/7758.bugfix.rst | 1 - changelog/7780.doc.rst | 1 - changelog/7802.trivial.rst | 1 - changelog/7808.breaking.rst | 1 - changelog/7872.doc.rst | 1 - changelog/7878.doc.rst | 1 - changelog/7911.bugfix.rst | 1 - changelog/7913.bugfix.rst | 1 - changelog/7938.improvement.rst | 1 - changelog/7951.bugfix.rst | 1 - changelog/7981.bugfix.rst | 1 - changelog/7988.deprecation.rst | 3 - changelog/8006.feature.rst | 8 -- changelog/8014.trivial.rst | 2 - changelog/8016.bugfix.rst | 1 - changelog/8023.improvement.rst | 1 - changelog/8032.improvement.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-6.2.0.rst | 76 +++++++++++ doc/en/builtin.rst | 10 ++ doc/en/changelog.rst | 197 ++++++++++++++++++++++++++++ doc/en/example/nonpython.rst | 2 +- doc/en/example/parametrize.rst | 6 +- doc/en/example/pythoncollection.rst | 6 +- doc/en/example/reportingdemo.rst | 2 +- doc/en/fixture.rst | 4 +- doc/en/getting-started.rst | 2 +- doc/en/reference.rst | 7 +- doc/en/tmpdir.rst | 4 +- doc/en/writing_plugins.rst | 8 +- 44 files changed, 302 insertions(+), 132 deletions(-) delete mode 100644 changelog/1265.improvement.rst delete mode 100644 changelog/2044.improvement.rst delete mode 100644 changelog/4824.bugfix.rst delete mode 100644 changelog/5299.feature.rst delete mode 100644 changelog/7425.feature.rst delete mode 100644 changelog/7429.doc.rst delete mode 100644 changelog/7469.deprecation.rst delete mode 100644 changelog/7469.improvement.rst delete mode 100644 changelog/7527.improvement.rst delete mode 100644 changelog/7530.deprecation.rst delete mode 100644 changelog/7615.improvement.rst delete mode 100644 changelog/7695.feature.rst delete mode 100644 changelog/7701.improvement.rst delete mode 100644 changelog/7710.improvement.rst delete mode 100644 changelog/7758.bugfix.rst delete mode 100644 changelog/7780.doc.rst delete mode 100644 changelog/7802.trivial.rst delete mode 100644 changelog/7808.breaking.rst delete mode 100644 changelog/7872.doc.rst delete mode 100644 changelog/7878.doc.rst delete mode 100644 changelog/7911.bugfix.rst delete mode 100644 changelog/7913.bugfix.rst delete mode 100644 changelog/7938.improvement.rst delete mode 100644 changelog/7951.bugfix.rst delete mode 100644 changelog/7981.bugfix.rst delete mode 100644 changelog/7988.deprecation.rst delete mode 100644 changelog/8006.feature.rst delete mode 100644 changelog/8014.trivial.rst delete mode 100644 changelog/8016.bugfix.rst delete mode 100644 changelog/8023.improvement.rst delete mode 100644 changelog/8032.improvement.rst create mode 100644 doc/en/announce/release-6.2.0.rst diff --git a/changelog/1265.improvement.rst b/changelog/1265.improvement.rst deleted file mode 100644 index 4e7d98c0a99..00000000000 --- a/changelog/1265.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Added an ``__str__`` implementation to the :class:`~pytest.pytester.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method. diff --git a/changelog/2044.improvement.rst b/changelog/2044.improvement.rst deleted file mode 100644 index c9e47c3f604..00000000000 --- a/changelog/2044.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS". diff --git a/changelog/4824.bugfix.rst b/changelog/4824.bugfix.rst deleted file mode 100644 index f2e6db7ab0f..00000000000 --- a/changelog/4824.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed quadratic behavior and improved performance of collection of items using autouse fixtures and xunit fixtures. diff --git a/changelog/5299.feature.rst b/changelog/5299.feature.rst deleted file mode 100644 index 7853e1833db..00000000000 --- a/changelog/5299.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -pytest now warns about unraisable exceptions and unhandled thread exceptions that occur in tests on Python>=3.8. -See :ref:`unraisable` for more information. diff --git a/changelog/7425.feature.rst b/changelog/7425.feature.rst deleted file mode 100644 index 47e6f4dbd30..00000000000 --- a/changelog/7425.feature.rst +++ /dev/null @@ -1,5 +0,0 @@ -New :fixture:`pytester` fixture, which is identical to :fixture:`testdir` but its methods return :class:`pathlib.Path` when appropriate instead of ``py.path.local``. - -This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future. - -Internally, the old :class:`Testdir <_pytest.pytester.Testdir>` is now a thin wrapper around :class:`Pytester <_pytest.pytester.Pytester>`, preserving the old interface. diff --git a/changelog/7429.doc.rst b/changelog/7429.doc.rst deleted file mode 100644 index e6376b727b2..00000000000 --- a/changelog/7429.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add more information and use cases about skipping doctests. diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst deleted file mode 100644 index 67d0b2bba46..00000000000 --- a/changelog/7469.deprecation.rst +++ /dev/null @@ -1,18 +0,0 @@ -Directly constructing/calling the following classes/functions is now deprecated: - -- ``_pytest.cacheprovider.Cache`` -- ``_pytest.cacheprovider.Cache.for_config()`` -- ``_pytest.cacheprovider.Cache.clear_cache()`` -- ``_pytest.cacheprovider.Cache.cache_dir_from_config()`` -- ``_pytest.capture.CaptureFixture`` -- ``_pytest.fixtures.FixtureRequest`` -- ``_pytest.fixtures.SubRequest`` -- ``_pytest.logging.LogCaptureFixture`` -- ``_pytest.pytester.Pytester`` -- ``_pytest.pytester.Testdir`` -- ``_pytest.recwarn.WarningsRecorder`` -- ``_pytest.recwarn.WarningsChecker`` -- ``_pytest.tmpdir.TempPathFactory`` -- ``_pytest.tmpdir.TempdirFactory`` - -These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0. diff --git a/changelog/7469.improvement.rst b/changelog/7469.improvement.rst deleted file mode 100644 index cbd75f05419..00000000000 --- a/changelog/7469.improvement.rst +++ /dev/null @@ -1,23 +0,0 @@ -It is now possible to construct a :class:`MonkeyPatch` object directly as ``pytest.MonkeyPatch()``, -in cases when the :fixture:`monkeypatch` fixture cannot be used. Previously some users imported it -from the private `_pytest.monkeypatch.MonkeyPatch` namespace. - -The types of builtin pytest fixtures are now exported so they may be used in type annotations of test functions. -The newly-exported types are: - -- ``pytest.FixtureRequest`` for the :fixture:`request` fixture. -- ``pytest.Cache`` for the :fixture:`cache` fixture. -- ``pytest.CaptureFixture[str]`` for the :fixture:`capfd` and :fixture:`capsys` fixtures. -- ``pytest.CaptureFixture[bytes]`` for the :fixture:`capfdbinary` and :fixture:`capsysbinary` fixtures. -- ``pytest.LogCaptureFixture`` for the :fixture:`caplog` fixture. -- ``pytest.Pytester`` for the :fixture:`pytester` fixture. -- ``pytest.Testdir`` for the :fixture:`testdir` fixture. -- ``pytest.TempdirFactory`` for the :fixture:`tmpdir_factory` fixture. -- ``pytest.TempPathFactory`` for the :fixture:`tmp_path_factory` fixture. -- ``pytest.MonkeyPatch`` for the :fixture:`monkeypatch` fixture. -- ``pytest.WarningsRecorder`` for the :fixture:`recwarn` fixture. - -Constructing them is not supported (except for `MonkeyPatch`); they are only meant for use in type annotations. -Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0. - -Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy. diff --git a/changelog/7527.improvement.rst b/changelog/7527.improvement.rst deleted file mode 100644 index 3a7e063fe6f..00000000000 --- a/changelog/7527.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -When a comparison between :func:`namedtuple ` instances of the same type fails, pytest now shows the differing field names (possibly nested) instead of their indexes. diff --git a/changelog/7530.deprecation.rst b/changelog/7530.deprecation.rst deleted file mode 100644 index 36a763e51f1..00000000000 --- a/changelog/7530.deprecation.rst +++ /dev/null @@ -1,4 +0,0 @@ -The ``--strict`` command-line option has been deprecated, use ``--strict-markers`` instead. - -We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing flag for all strictness -related options (``--strict-markers`` and ``--strict-config`` at the moment, more might be introduced in the future). diff --git a/changelog/7615.improvement.rst b/changelog/7615.improvement.rst deleted file mode 100644 index fcf9a1a9b42..00000000000 --- a/changelog/7615.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -:meth:`Node.warn <_pytest.nodes.Node.warn>` now permits any subclass of :class:`Warning`, not just :class:`PytestWarning `. diff --git a/changelog/7695.feature.rst b/changelog/7695.feature.rst deleted file mode 100644 index ec8632fc82a..00000000000 --- a/changelog/7695.feature.rst +++ /dev/null @@ -1,19 +0,0 @@ -A new hook was added, `pytest_markeval_namespace` which should return a dictionary. -This dictionary will be used to augment the "global" variables available to evaluate skipif/xfail/xpass markers. - -Pseudo example - -``conftest.py``: - -.. code-block:: python - - def pytest_markeval_namespace(): - return {"color": "red"} - -``test_func.py``: - -.. code-block:: python - - @pytest.mark.skipif("color == 'blue'", reason="Color is not red") - def test_func(): - assert False diff --git a/changelog/7701.improvement.rst b/changelog/7701.improvement.rst deleted file mode 100644 index e214be9e3fe..00000000000 --- a/changelog/7701.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Improved reporting when using ``--collected-only``. It will now show the number of collected tests in the summary stats. diff --git a/changelog/7710.improvement.rst b/changelog/7710.improvement.rst deleted file mode 100644 index 91b703ab60f..00000000000 --- a/changelog/7710.improvement.rst +++ /dev/null @@ -1,4 +0,0 @@ -Use strict equality comparison for non-numeric types in :func:`pytest.approx` instead of -raising :class:`TypeError`. - -This was the undocumented behavior before 3.7, but is now officially a supported feature. diff --git a/changelog/7758.bugfix.rst b/changelog/7758.bugfix.rst deleted file mode 100644 index a3119b46c0d..00000000000 --- a/changelog/7758.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed an issue where some files in packages are getting lost from ``--lf`` even though they contain tests that failed. Regressed in pytest 5.4.0. diff --git a/changelog/7780.doc.rst b/changelog/7780.doc.rst deleted file mode 100644 index 631873b156e..00000000000 --- a/changelog/7780.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Classes which should not be inherited from are now marked ``final class`` in the API reference. diff --git a/changelog/7802.trivial.rst b/changelog/7802.trivial.rst deleted file mode 100644 index 1f8bc2c9dc6..00000000000 --- a/changelog/7802.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -The ``attrs`` dependency requirement is now >=19.2.0 instead of >=17.4.0. diff --git a/changelog/7808.breaking.rst b/changelog/7808.breaking.rst deleted file mode 100644 index 114b6a382cf..00000000000 --- a/changelog/7808.breaking.rst +++ /dev/null @@ -1 +0,0 @@ -pytest now supports python3.6+ only. diff --git a/changelog/7872.doc.rst b/changelog/7872.doc.rst deleted file mode 100644 index 46236acbf2a..00000000000 --- a/changelog/7872.doc.rst +++ /dev/null @@ -1 +0,0 @@ -``_pytest.config.argparsing.Parser.addini()`` accepts explicit ``None`` and ``"string"``. diff --git a/changelog/7878.doc.rst b/changelog/7878.doc.rst deleted file mode 100644 index ff5d00d6c02..00000000000 --- a/changelog/7878.doc.rst +++ /dev/null @@ -1 +0,0 @@ -In pull request section, ask to commit after editing changelog and authors file. diff --git a/changelog/7911.bugfix.rst b/changelog/7911.bugfix.rst deleted file mode 100644 index 1ef783fbabb..00000000000 --- a/changelog/7911.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites. diff --git a/changelog/7913.bugfix.rst b/changelog/7913.bugfix.rst deleted file mode 100644 index f42e7cb9cbd..00000000000 --- a/changelog/7913.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a crash or hang in :meth:`pytester.spawn <_pytest.pytester.Pytester.spawn>` when the :mod:`readline` module is involved. diff --git a/changelog/7938.improvement.rst b/changelog/7938.improvement.rst deleted file mode 100644 index ffe612d0da6..00000000000 --- a/changelog/7938.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -New ``--sw-skip`` argument which is a shorthand for ``--stepwise-skip``. diff --git a/changelog/7951.bugfix.rst b/changelog/7951.bugfix.rst deleted file mode 100644 index 56c71db7839..00000000000 --- a/changelog/7951.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed handling of recursive symlinks when collecting tests. diff --git a/changelog/7981.bugfix.rst b/changelog/7981.bugfix.rst deleted file mode 100644 index 0a254b5d49d..00000000000 --- a/changelog/7981.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed symlinked directories not being followed during collection. Regressed in pytest 6.1.0. diff --git a/changelog/7988.deprecation.rst b/changelog/7988.deprecation.rst deleted file mode 100644 index 34f646c9ab4..00000000000 --- a/changelog/7988.deprecation.rst +++ /dev/null @@ -1,3 +0,0 @@ -The ``@pytest.yield_fixture`` decorator/function is now deprecated. Use :func:`pytest.fixture` instead. - -``yield_fixture`` has been an alias for ``fixture`` for a very long time, so can be search/replaced safely. diff --git a/changelog/8006.feature.rst b/changelog/8006.feature.rst deleted file mode 100644 index 0203689ba4b..00000000000 --- a/changelog/8006.feature.rst +++ /dev/null @@ -1,8 +0,0 @@ -It is now possible to construct a :class:`~pytest.MonkeyPatch` object directly as ``pytest.MonkeyPatch()``, -in cases when the :fixture:`monkeypatch` fixture cannot be used. Previously some users imported it -from the private `_pytest.monkeypatch.MonkeyPatch` namespace. - -Additionally, :meth:`MonkeyPatch.context ` is now a classmethod, -and can be used as ``with MonkeyPatch.context() as mp: ...``. This is the recommended way to use -``MonkeyPatch`` directly, since unlike the ``monkeypatch`` fixture, an instance created directly -is not ``undo()``-ed automatically. diff --git a/changelog/8014.trivial.rst b/changelog/8014.trivial.rst deleted file mode 100644 index 3b9fb7bc271..00000000000 --- a/changelog/8014.trivial.rst +++ /dev/null @@ -1,2 +0,0 @@ -`.pyc` files created by pytest's assertion rewriting now conform to the newer PEP-552 format on Python>=3.7. -(These files are internal and only interpreted by pytest itself.) diff --git a/changelog/8016.bugfix.rst b/changelog/8016.bugfix.rst deleted file mode 100644 index 94539af5c97..00000000000 --- a/changelog/8016.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed only one doctest being collected when using ``pytest --doctest-modules path/to/an/__init__.py``. diff --git a/changelog/8023.improvement.rst b/changelog/8023.improvement.rst deleted file mode 100644 index c791dabc72d..00000000000 --- a/changelog/8023.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``'node_modules'`` to default value for :confval:`norecursedirs`. diff --git a/changelog/8032.improvement.rst b/changelog/8032.improvement.rst deleted file mode 100644 index 76789ea5097..00000000000 --- a/changelog/8032.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -:meth:`doClassCleanups ` (introduced in :mod:`unittest` in Python and 3.8) is now called appropriately. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index f0e44107b69..003a0a1a9ca 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.2.0 release-6.1.2 release-6.1.1 release-6.1.0 diff --git a/doc/en/announce/release-6.2.0.rst b/doc/en/announce/release-6.2.0.rst new file mode 100644 index 00000000000..af16b830ddd --- /dev/null +++ b/doc/en/announce/release-6.2.0.rst @@ -0,0 +1,76 @@ +pytest-6.2.0 +======================================= + +The pytest team is proud to announce the 6.2.0 release! + +This release contains new features, improvements, bug fixes, and breaking changes, so users +are encouraged to take a look at the CHANGELOG carefully: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Adam Johnson +* Albert Villanova del Moral +* Anthony Sottile +* Anton +* Ariel Pillemer +* Bruno Oliveira +* Charles Aracil +* Christine M +* Christine Mecklenborg +* Cserna Zsolt +* Dominic Mortlock +* Emiel van de Laar +* Florian Bruhin +* Garvit Shubham +* Gustavo Camargo +* Hugo Martins +* Hugo van Kemenade +* Jakob van Santen +* Josias Aurel +* Jürgen Gmach +* Karthikeyan Singaravelan +* Katarzyna +* Kyle Altendorf +* Manuel Mariñez +* Matthew Hughes +* Matthias Gabriel +* Max Voitko +* Maximilian Cosmo Sitter +* Mikhail Fesenko +* Nimesh Vashistha +* Pedro Algarvio +* Petter Strandmark +* Prakhar Gurunani +* Prashant Sharma +* Ran Benita +* Ronny Pfannschmidt +* Sanket Duthade +* Shubham Adep +* Simon K +* Tanvi Mehta +* Thomas Grainger +* Tim Hoffmann +* Vasilis Gerakaris +* William Jamir Silva +* Zac Hatfield-Dodds +* crricks +* dependabot[bot] +* duthades +* frankgerhardt +* kwgchi +* mickeypash +* symonk + + +Happy testing, +The pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 1d7fe76e3b7..5c7c8dfe666 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -158,6 +158,11 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a function invocation, created as a sub directory of the base temporary directory. + By default, a new base temporary directory is created each test session, + and old bases are removed after 3 sessions, to aid in debugging. If + ``--basetemp`` is used then it is cleared each session. See :ref:`base + temporary directory`. + The returned object is a `py.path.local`_ path object. .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html @@ -167,6 +172,11 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a function invocation, created as a sub directory of the base temporary directory. + By default, a new base temporary directory is created each test session, + and old bases are removed after 3 sessions, to aid in debugging. If + ``--basetemp`` is used then it is cleared each session. See :ref:`base + temporary directory`. + The returned object is a :class:`pathlib.Path` object. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 3f14921a80a..77340b1bb84 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,203 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 6.2.0 (2020-12-12) +========================= + +Breaking Changes +---------------- + +- `#7808 `_: pytest now supports python3.6+ only. + + + +Deprecations +------------ + +- `#7469 `_: Directly constructing/calling the following classes/functions is now deprecated: + + - ``_pytest.cacheprovider.Cache`` + - ``_pytest.cacheprovider.Cache.for_config()`` + - ``_pytest.cacheprovider.Cache.clear_cache()`` + - ``_pytest.cacheprovider.Cache.cache_dir_from_config()`` + - ``_pytest.capture.CaptureFixture`` + - ``_pytest.fixtures.FixtureRequest`` + - ``_pytest.fixtures.SubRequest`` + - ``_pytest.logging.LogCaptureFixture`` + - ``_pytest.pytester.Pytester`` + - ``_pytest.pytester.Testdir`` + - ``_pytest.recwarn.WarningsRecorder`` + - ``_pytest.recwarn.WarningsChecker`` + - ``_pytest.tmpdir.TempPathFactory`` + - ``_pytest.tmpdir.TempdirFactory`` + + These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0. + + +- `#7530 `_: The ``--strict`` command-line option has been deprecated, use ``--strict-markers`` instead. + + We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing flag for all strictness + related options (``--strict-markers`` and ``--strict-config`` at the moment, more might be introduced in the future). + + +- `#7988 `_: The ``@pytest.yield_fixture`` decorator/function is now deprecated. Use :func:`pytest.fixture` instead. + + ``yield_fixture`` has been an alias for ``fixture`` for a very long time, so can be search/replaced safely. + + + +Features +-------- + +- `#5299 `_: pytest now warns about unraisable exceptions and unhandled thread exceptions that occur in tests on Python>=3.8. + See :ref:`unraisable` for more information. + + +- `#7425 `_: New :fixture:`pytester` fixture, which is identical to :fixture:`testdir` but its methods return :class:`pathlib.Path` when appropriate instead of ``py.path.local``. + + This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future. + + Internally, the old :class:`Testdir <_pytest.pytester.Testdir>` is now a thin wrapper around :class:`Pytester <_pytest.pytester.Pytester>`, preserving the old interface. + + +- `#7695 `_: A new hook was added, `pytest_markeval_namespace` which should return a dictionary. + This dictionary will be used to augment the "global" variables available to evaluate skipif/xfail/xpass markers. + + Pseudo example + + ``conftest.py``: + + .. code-block:: python + + def pytest_markeval_namespace(): + return {"color": "red"} + + ``test_func.py``: + + .. code-block:: python + + @pytest.mark.skipif("color == 'blue'", reason="Color is not red") + def test_func(): + assert False + + +- `#8006 `_: It is now possible to construct a :class:`~pytest.MonkeyPatch` object directly as ``pytest.MonkeyPatch()``, + in cases when the :fixture:`monkeypatch` fixture cannot be used. Previously some users imported it + from the private `_pytest.monkeypatch.MonkeyPatch` namespace. + + Additionally, :meth:`MonkeyPatch.context ` is now a classmethod, + and can be used as ``with MonkeyPatch.context() as mp: ...``. This is the recommended way to use + ``MonkeyPatch`` directly, since unlike the ``monkeypatch`` fixture, an instance created directly + is not ``undo()``-ed automatically. + + + +Improvements +------------ + +- `#1265 `_: Added an ``__str__`` implementation to the :class:`~pytest.pytester.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method. + + +- `#2044 `_: Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS". + + +- `#7469 `_ The types of builtin pytest fixtures are now exported so they may be used in type annotations of test functions. + The newly-exported types are: + + - ``pytest.FixtureRequest`` for the :fixture:`request` fixture. + - ``pytest.Cache`` for the :fixture:`cache` fixture. + - ``pytest.CaptureFixture[str]`` for the :fixture:`capfd` and :fixture:`capsys` fixtures. + - ``pytest.CaptureFixture[bytes]`` for the :fixture:`capfdbinary` and :fixture:`capsysbinary` fixtures. + - ``pytest.LogCaptureFixture`` for the :fixture:`caplog` fixture. + - ``pytest.Pytester`` for the :fixture:`pytester` fixture. + - ``pytest.Testdir`` for the :fixture:`testdir` fixture. + - ``pytest.TempdirFactory`` for the :fixture:`tmpdir_factory` fixture. + - ``pytest.TempPathFactory`` for the :fixture:`tmp_path_factory` fixture. + - ``pytest.MonkeyPatch`` for the :fixture:`monkeypatch` fixture. + - ``pytest.WarningsRecorder`` for the :fixture:`recwarn` fixture. + + Constructing them is not supported (except for `MonkeyPatch`); they are only meant for use in type annotations. + Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0. + + Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy. + + +- `#7527 `_: When a comparison between :func:`namedtuple ` instances of the same type fails, pytest now shows the differing field names (possibly nested) instead of their indexes. + + +- `#7615 `_: :meth:`Node.warn <_pytest.nodes.Node.warn>` now permits any subclass of :class:`Warning`, not just :class:`PytestWarning `. + + +- `#7701 `_: Improved reporting when using ``--collected-only``. It will now show the number of collected tests in the summary stats. + + +- `#7710 `_: Use strict equality comparison for non-numeric types in :func:`pytest.approx` instead of + raising :class:`TypeError`. + + This was the undocumented behavior before 3.7, but is now officially a supported feature. + + +- `#7938 `_: New ``--sw-skip`` argument which is a shorthand for ``--stepwise-skip``. + + +- `#8023 `_: Added ``'node_modules'`` to default value for :confval:`norecursedirs`. + + +- `#8032 `_: :meth:`doClassCleanups ` (introduced in :mod:`unittest` in Python and 3.8) is now called appropriately. + + + +Bug Fixes +--------- + +- `#4824 `_: Fixed quadratic behavior and improved performance of collection of items using autouse fixtures and xunit fixtures. + + +- `#7758 `_: Fixed an issue where some files in packages are getting lost from ``--lf`` even though they contain tests that failed. Regressed in pytest 5.4.0. + + +- `#7911 `_: Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites. + + +- `#7913 `_: Fixed a crash or hang in :meth:`pytester.spawn <_pytest.pytester.Pytester.spawn>` when the :mod:`readline` module is involved. + + +- `#7951 `_: Fixed handling of recursive symlinks when collecting tests. + + +- `#7981 `_: Fixed symlinked directories not being followed during collection. Regressed in pytest 6.1.0. + + +- `#8016 `_: Fixed only one doctest being collected when using ``pytest --doctest-modules path/to/an/__init__.py``. + + + +Improved Documentation +---------------------- + +- `#7429 `_: Add more information and use cases about skipping doctests. + + +- `#7780 `_: Classes which should not be inherited from are now marked ``final class`` in the API reference. + + +- `#7872 `_: ``_pytest.config.argparsing.Parser.addini()`` accepts explicit ``None`` and ``"string"``. + + +- `#7878 `_: In pull request section, ask to commit after editing changelog and authors file. + + + +Trivial/Internal Changes +------------------------ + +- `#7802 `_: The ``attrs`` dependency requirement is now >=19.2.0 instead of >=17.4.0. + + +- `#8014 `_: `.pyc` files created by pytest's assertion rewriting now conform to the newer PEP-552 format on Python>=3.7. + (These files are internal and only interpreted by pytest itself.) + + pytest 6.1.2 (2020-10-28) ========================= diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index 558c56772f1..a3477fe1e1d 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -102,4 +102,4 @@ interesting to just look at the collection tree: - ========================== 2 tests found in 0.12s =========================== + ======================== 2 tests collected in 0.12s ======================== diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index d5a11b45192..6e2f53984ee 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -175,7 +175,7 @@ objects, they are still using the default pytest representation: - ========================== 8 tests found in 0.12s =========================== + ======================== 8 tests collected in 0.12s ======================== In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs together with the actual data, instead of listing them separately. @@ -252,7 +252,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia - ========================== 4 tests found in 0.12s =========================== + ======================== 4 tests collected in 0.12s ======================== Note that we told ``metafunc.parametrize()`` that your scenario values should be considered class-scoped. With pytest-2.3 this leads to a @@ -328,7 +328,7 @@ Let's first see how it looks like at collection time: - ========================== 2/2 tests found in 0.12s =========================== + ======================== 2 tests collected in 0.12s ======================== And then when we run the test: diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index f7917b790ef..a6ce2e742e5 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -157,7 +157,7 @@ The test collection would look like this: - ========================== 2 tests found in 0.12s =========================== + ======================== 2 tests collected in 0.12s ======================== You can check for multiple glob patterns by adding a space between the patterns: @@ -220,7 +220,7 @@ You can always peek at the collection tree without running tests like this: - ========================== 3 tests found in 0.12s =========================== + ======================== 3 tests collected in 0.12s ======================== .. _customizing-test-collection: @@ -296,7 +296,7 @@ file will be left out: rootdir: $REGENDOC_TMPDIR, configfile: pytest.ini collected 0 items - ========================== no tests found in 0.12s =========================== + ======================= no tests collected in 0.12s ======================== It's also possible to ignore files based on Unix shell-style wildcards by adding patterns to :globalvar:`collect_ignore_glob`. diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index f1b973f3b33..6e7dbe49683 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -446,7 +446,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_reinterpret_fails_with_print_for_the_fun_of_it(self): items = [1, 2, 3] - print("items is {!r}".format(items)) + print(f"items is {items!r}") > a, b = items.pop() E TypeError: cannot unpack non-iterable int object diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 22b86ffcafc..963fc32e6b0 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -919,7 +919,7 @@ Running the above tests results in the following test IDs being used: - ========================== 10 tests found in 0.12s =========================== + ======================= 10 tests collected in 0.12s ======================== .. _`fixture-parametrize-marks`: @@ -958,7 +958,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``: test_fixture_marks.py::test_data[0] PASSED [ 33%] test_fixture_marks.py::test_data[1] PASSED [ 66%] - test_fixture_marks.py::test_data[2] SKIPPED [100%] + test_fixture_marks.py::test_data[2] SKIPPED (unconditional skip) [100%] ======================= 2 passed, 1 skipped in 0.12s ======================= diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 4814b850586..fe15c218cde 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 6.1.2 + pytest 6.2.0 .. _`simpletest`: diff --git a/doc/en/reference.rst b/doc/en/reference.rst index f62b51414b4..8aa95ca6448 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1749,7 +1749,8 @@ All the command-line flags can be obtained by running ``pytest --help``:: failures. --sw, --stepwise exit on test failure and continue from last failing test next time - --stepwise-skip ignore the first failing test but stop on the next + --sw-skip, --stepwise-skip + ignore the first failing test but stop on the next failing test reporting: @@ -1791,9 +1792,9 @@ All the command-line flags can be obtained by running ``pytest --help``:: --maxfail=num exit after first num failures or errors. --strict-config any warnings encountered while parsing the `pytest` section of the configuration file raise errors. - --strict-markers, --strict - markers not registered in the `markers` section of + --strict-markers markers not registered in the `markers` section of the configuration file raise errors. + --strict (deprecated) alias to --strict-markers. -c file load configuration from `file` instead of trying to locate one of the implicit configuration files. --continue-on-collection-errors diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index c8da5877b28..adcba02cb15 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -61,7 +61,7 @@ Running this would result in a passed test except for the last > assert 0 E assert 0 - test_tmp_path.py:13: AssertionError + test_tmp_path.py:11: AssertionError ========================= short test summary info ========================== FAILED test_tmp_path.py::test_create_file - assert 0 ============================ 1 failed in 0.12s ============================= @@ -129,7 +129,7 @@ Running this would result in a passed test except for the last > assert 0 E assert 0 - test_tmpdir.py:9: AssertionError + test_tmpdir.py:6: AssertionError ========================= short test summary info ========================== FAILED test_tmpdir.py::test_create_file - assert 0 ============================ 1 failed in 0.12s ============================= diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 41967525b33..908366d5290 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -449,13 +449,7 @@ Additionally it is possible to copy examples for an example folder before runnin test_example.py .. [100%] - ============================= warnings summary ============================= - test_example.py::test_plugin - $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time - testdir.copy_example("test_example.py") - - -- Docs: https://docs.pytest.org/en/stable/warnings.html - ======================= 2 passed, 1 warning in 0.12s ======================= + ============================ 2 passed in 0.12s ============================= For more information about the result object that ``runpytest()`` returns, and the methods that it provides please check out the :py:class:`RunResult From f854cf66f4ad866d27f85e6fcc3b476a835ca3e7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 13 Dec 2020 10:35:11 -0300 Subject: [PATCH 821/823] Merge pull request #8123 from nicoddemus/import-mismatch-unc Compare also paths on Windows when considering ImportPathMismatchError --- changelog/7678.bugfix.rst | 2 ++ src/_pytest/pathlib.py | 16 +++++++++++++++- testing/test_pathlib.py | 21 +++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 changelog/7678.bugfix.rst diff --git a/changelog/7678.bugfix.rst b/changelog/7678.bugfix.rst new file mode 100644 index 00000000000..4adc6ffd119 --- /dev/null +++ b/changelog/7678.bugfix.rst @@ -0,0 +1,2 @@ +Fixed bug where ``ImportPathMismatchError`` would be raised for files compiled in +the host and loaded later from an UNC mounted path (Windows). diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 6a36ae17ab2..8875a28f84b 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -543,7 +543,7 @@ def import_path( module_file = module_file[: -(len(os.path.sep + "__init__.py"))] try: - is_same = os.path.samefile(str(path), module_file) + is_same = _is_same(str(path), module_file) except FileNotFoundError: is_same = False @@ -553,6 +553,20 @@ def import_path( return mod +# Implement a special _is_same function on Windows which returns True if the two filenames +# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678). +if sys.platform.startswith("win"): + + def _is_same(f1: str, f2: str) -> bool: + return Path(f1) == Path(f2) or os.path.samefile(f1, f2) + + +else: + + def _is_same(f1: str, f2: str) -> bool: + return os.path.samefile(f1, f2) + + def resolve_package_path(path: Path) -> Optional[Path]: """Return the Python package path by looking for the last directory upwards which still contains an __init__.py. diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 0507e3d6866..f60b9f26369 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -7,6 +7,7 @@ import py import pytest +from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import bestrelpath from _pytest.pathlib import commonpath from _pytest.pathlib import ensure_deletable @@ -414,3 +415,23 @@ def test_visit_ignores_errors(tmpdir) -> None: "bar", "foo", ] + + +@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only") +def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + """ + import_file() should not raise ImportPathMismatchError if the paths are exactly + equal on Windows. It seems directories mounted as UNC paths make os.path.samefile + return False, even when they are clearly equal. + """ + module_path = tmp_path.joinpath("my_module.py") + module_path.write_text("def foo(): return 42") + monkeypatch.syspath_prepend(tmp_path) + + with monkeypatch.context() as mp: + # Forcibly make os.path.samefile() return False here to ensure we are comparing + # the paths too. Using a context to narrow the patch as much as possible given + # this is an important system function. + mp.setattr(os.path, "samefile", lambda x, y: False) + module = import_path(module_path) + assert getattr(module, "foo")() == 42 From 8354995abc4c4f913a8266fb53b7d2a4e93e4050 Mon Sep 17 00:00:00 2001 From: Jakob van Santen Date: Tue, 15 Dec 2020 12:49:29 +0100 Subject: [PATCH 822/823] python_api: handle array-like args in approx() (#8137) --- changelog/8132.bugfix.rst | 10 ++++++++++ src/_pytest/python_api.py | 33 +++++++++++++++++++++++++++------ testing/python/approx.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 changelog/8132.bugfix.rst diff --git a/changelog/8132.bugfix.rst b/changelog/8132.bugfix.rst new file mode 100644 index 00000000000..5be5e567491 --- /dev/null +++ b/changelog/8132.bugfix.rst @@ -0,0 +1,10 @@ +Fixed regression in ``approx``: in 6.2.0 ``approx`` no longer raises +``TypeError`` when dealing with non-numeric types, falling back to normal comparison. +Before 6.2.0, array types like tf.DeviceArray fell through to the scalar case, +and happened to compare correctly to a scalar if they had only one element. +After 6.2.0, these types began failing, because they inherited neither from +standard Python number hierarchy nor from ``numpy.ndarray``. + +``approx`` now converts arguments to ``numpy.ndarray`` if they expose the array +protocol and are not scalars. This treats array-like objects like numpy arrays, +regardless of size. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index bae2076892b..81ce4f89539 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -15,9 +15,14 @@ from typing import Pattern from typing import Tuple from typing import Type +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union +if TYPE_CHECKING: + from numpy import ndarray + + import _pytest._code from _pytest.compat import final from _pytest.compat import STRING_TYPES @@ -232,10 +237,11 @@ def __repr__(self) -> str: def __eq__(self, actual) -> bool: """Return whether the given value is equal to the expected value within the pre-specified tolerance.""" - if _is_numpy_array(actual): + asarray = _as_numpy_array(actual) + if asarray is not None: # Call ``__eq__()`` manually to prevent infinite-recursion with # numpy<1.13. See #3748. - return all(self.__eq__(a) for a in actual.flat) + return all(self.__eq__(a) for a in asarray.flat) # Short-circuit exact equality. if actual == self.expected: @@ -521,6 +527,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: elif isinstance(expected, Mapping): cls = ApproxMapping elif _is_numpy_array(expected): + expected = _as_numpy_array(expected) cls = ApproxNumpy elif ( isinstance(expected, Iterable) @@ -536,16 +543,30 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: def _is_numpy_array(obj: object) -> bool: - """Return true if the given object is a numpy array. + """ + Return true if the given object is implicitly convertible to ndarray, + and numpy is already imported. + """ + return _as_numpy_array(obj) is not None + - A special effort is made to avoid importing numpy unless it's really necessary. +def _as_numpy_array(obj: object) -> Optional["ndarray"]: + """ + Return an ndarray if the given object is implicitly convertible to ndarray, + and numpy is already imported, otherwise None. """ import sys np: Any = sys.modules.get("numpy") if np is not None: - return isinstance(obj, np.ndarray) - return False + # avoid infinite recursion on numpy scalars, which have __array__ + if np.isscalar(obj): + return None + elif isinstance(obj, np.ndarray): + return obj + elif hasattr(obj, "__array__") or hasattr("obj", "__array_interface__"): + return np.asarray(obj) + return None # builtin pytest.raises helper diff --git a/testing/python/approx.py b/testing/python/approx.py index 91c1f3f85de..e76d6b774d6 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -447,6 +447,36 @@ def test_numpy_array_wrong_shape(self): assert a12 != approx(a21) assert a21 != approx(a12) + def test_numpy_array_protocol(self): + """ + array-like objects such as tensorflow's DeviceArray are handled like ndarray. + See issue #8132 + """ + np = pytest.importorskip("numpy") + + class DeviceArray: + def __init__(self, value, size): + self.value = value + self.size = size + + def __array__(self): + return self.value * np.ones(self.size) + + class DeviceScalar: + def __init__(self, value): + self.value = value + + def __array__(self): + return np.array(self.value) + + expected = 1 + actual = 1 + 1e-6 + assert approx(expected) == DeviceArray(actual, size=1) + assert approx(expected) == DeviceArray(actual, size=2) + assert approx(expected) == DeviceScalar(actual) + assert approx(DeviceScalar(expected)) == actual + assert approx(DeviceScalar(expected)) == DeviceScalar(actual) + def test_doctests(self, mocked_doctest_runner) -> None: import doctest From d3971c30f4d0f1890a372af3b98de41ee555dcb3 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Tue, 15 Dec 2020 13:06:34 +0000 Subject: [PATCH 823/823] Prepare release version 6.2.1 --- changelog/7678.bugfix.rst | 2 -- changelog/8132.bugfix.rst | 10 ---------- doc/en/announce/index.rst | 1 + doc/en/announce/release-6.2.1.rst | 20 ++++++++++++++++++++ doc/en/changelog.rst | 22 ++++++++++++++++++++++ doc/en/getting-started.rst | 2 +- 6 files changed, 44 insertions(+), 13 deletions(-) delete mode 100644 changelog/7678.bugfix.rst delete mode 100644 changelog/8132.bugfix.rst create mode 100644 doc/en/announce/release-6.2.1.rst diff --git a/changelog/7678.bugfix.rst b/changelog/7678.bugfix.rst deleted file mode 100644 index 4adc6ffd119..00000000000 --- a/changelog/7678.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed bug where ``ImportPathMismatchError`` would be raised for files compiled in -the host and loaded later from an UNC mounted path (Windows). diff --git a/changelog/8132.bugfix.rst b/changelog/8132.bugfix.rst deleted file mode 100644 index 5be5e567491..00000000000 --- a/changelog/8132.bugfix.rst +++ /dev/null @@ -1,10 +0,0 @@ -Fixed regression in ``approx``: in 6.2.0 ``approx`` no longer raises -``TypeError`` when dealing with non-numeric types, falling back to normal comparison. -Before 6.2.0, array types like tf.DeviceArray fell through to the scalar case, -and happened to compare correctly to a scalar if they had only one element. -After 6.2.0, these types began failing, because they inherited neither from -standard Python number hierarchy nor from ``numpy.ndarray``. - -``approx`` now converts arguments to ``numpy.ndarray`` if they expose the array -protocol and are not scalars. This treats array-like objects like numpy arrays, -regardless of size. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 003a0a1a9ca..e7cac2a1c41 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.2.1 release-6.2.0 release-6.1.2 release-6.1.1 diff --git a/doc/en/announce/release-6.2.1.rst b/doc/en/announce/release-6.2.1.rst new file mode 100644 index 00000000000..f9e71618351 --- /dev/null +++ b/doc/en/announce/release-6.2.1.rst @@ -0,0 +1,20 @@ +pytest-6.2.1 +======================================= + +pytest 6.2.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Jakob van Santen +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 77340b1bb84..6d66ad1d8dc 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,28 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 6.2.1 (2020-12-15) +========================= + +Bug Fixes +--------- + +- `#7678 `_: Fixed bug where ``ImportPathMismatchError`` would be raised for files compiled in + the host and loaded later from an UNC mounted path (Windows). + + +- `#8132 `_: Fixed regression in ``approx``: in 6.2.0 ``approx`` no longer raises + ``TypeError`` when dealing with non-numeric types, falling back to normal comparison. + Before 6.2.0, array types like tf.DeviceArray fell through to the scalar case, + and happened to compare correctly to a scalar if they had only one element. + After 6.2.0, these types began failing, because they inherited neither from + standard Python number hierarchy nor from ``numpy.ndarray``. + + ``approx`` now converts arguments to ``numpy.ndarray`` if they expose the array + protocol and are not scalars. This treats array-like objects like numpy arrays, + regardless of size. + + pytest 6.2.0 (2020-12-12) ========================= diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index fe15c218cde..09410585dc7 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 6.2.0 + pytest 6.2.1 .. _`simpletest`: